Performancehighloading
Load non-critical code when content approaches the viewport
rule · import-on-visibility
Import-on-visibility starts loading code when an offscreen section is close enough to matter. Patterns.dev's import-on-visibility pattern (opens in a new tab) and Intersection Observer (opens in a new tab) make it a practical way to keep the initial page lean without making later sections feel unfinished.
Code Examples
Plain JavaScript with Intersection Observer
JavaScript
const mountPoint = document.querySelector('#reviews')
let loaded = false
const observer = new IntersectionObserver(
async ([entry]) => {
if (!entry.isIntersecting || loaded) return
loaded = true
observer.disconnect()
const { mountReviews } = await import('./reviews.js')
mountReviews(mountPoint)
},
{ rootMargin: '400px 0px' }
)
observer.observe(mountPoint)React Example
TSX
import { useEffect, useRef, useState } from 'react'
export function DeferredChart() {
const containerRef = useRef<HTMLDivElement | null>(null)
const [Chart, setChart] = useState<null | React.ComponentType>(null)
useEffect(() => {
const node = containerRef.current
if (!node) return
const observer = new IntersectionObserver(
async ([entry]) => {
if (!entry.isIntersecting) return
observer.disconnect()
const mod = await import('./sales-chart')
setChart(() => mod.SalesChart)
},
{ rootMargin: '500px 0px' }
)
observer.observe(node)
return () => observer.disconnect()
}, [])
return (
<div ref={containerRef} style={{ minHeight: 320 }}>
{Chart ? <Chart /> : <ChartSkeleton />}
</div>
)
}Why It Matters
- Reduced first-load cost: Offscreen components do not compete with critical content during the initial route load.
- Better scroll journeys: Heavy sections can load just before the user reaches them instead of blocking first paint.
- Controlled timing:
rootMarginlets you decide how early to fetch based on the weight of the deferred component, which is why this pattern complements lazy loading instead of replacing it. - Better fit for long pages: Marketing pages, dashboards, feeds, and article templates often contain expensive sections well below the fold.
Implementation Guidance
- Use a placeholder or skeleton with reserved height so the section does not shift layout when it loads.
- Start with a
rootMarginbetween300pxand1000pxfor heavier modules, then tune it based on asset weight and scroll speed. - Disconnect the observer after loading so the import runs once.
- Keep accessibility intact: headings, landmarks, and focus order should still make sense before the module hydrates, and PageSpeed Insights (opens in a new tab) is a good way to verify the route-level impact afterward.
Verification
Use the Chrome DevTools Performance panel (opens in a new tab) or a throttled waterfall trace to confirm the deferred code leaves the initial path and then arrives early enough during scroll.
- Confirm the deferred section's code is not part of the initial route bundle or first-load request set.
- Scroll through the page on a throttled mobile profile and verify the module request starts before the section enters the viewport.
- Check that the placeholder reserves space so the section loads without causing CLS.
- Tune
rootMarginuntil the section appears ready when reached, typically somewhere between300pxand1000pxbefore visibility for heavier modules. - Re-measure the route and confirm initial JS cost, main-thread work, or page weight improves without introducing visible loading jank later in the scroll.