Lazy Loading: The Patterns That Work and the Ones That Backfire
Lazy loading is the practice of deferring the loading of non critical resources until they are needed or near the viewport. For images, it means the browser does not fetch them until the user scrolls near them. For JavaScript modules, it means code is not downloaded until the feature that requires it is used. When applied correctly, lazy loading reduces initial page weight and improves time to interactive. When applied incorrectly, it delays the content users need immediately.
What you need to know
- Never lazy load the LCP image. The LCP element should load as fast as possible. Adding
loading="lazy"to it directly delays the metric that most affects perceived performance. - Lazy load all below fold images. The native
loading="lazy"attribute is the correct implementation, not an intersection observer library. - Code splitting at the route level is the highest impact JavaScript lazy loading pattern. Loading page specific code only when that page is visited reduces the initial bundle significantly.
- Setting explicit width and height on lazy loaded images prevents layout shift (CLS). Without dimensions, the browser cannot reserve space and the layout jumps as images load.
- Lazy loading JavaScript for features that users interact with immediately creates noticeable response delays. Lazy load only features that are optional or infrequently accessed.
The core argument
The two lazy loading patterns that create the most damage are lazy loading the hero image and lazy loading the JavaScript for core interactive features. Both feel like performance optimizations because they defer loading. Both produce the opposite result: the hero image with loading="lazy" directly delays the Largest Contentful Paint metric, and the lazy loaded interaction handler produces a noticeable click delay the first time the feature is used. These patterns appear in production codebases regularly because the downside is not visible in development on a fast machine with a warm cache.
The correct frame for lazy loading is "defer what users do not need immediately." Below fold images are the clearest case: the user cannot see them on initial page load, so loading them before the page is interactive wastes bandwidth and computation that could go toward the content the user actually sees. The native loading="lazy" attribute handles this correctly and efficiently without JavaScript overhead. Code splitting at the route level is the correct frame for JavaScript lazy loading: the code for the settings page, the reporting dashboard, and the admin panel should not be in the initial bundle that every user downloads on landing, because most users never visit those pages.
The dangerous pattern is lazy loading components that are critical to the page's interactive functionality but happen to be below a Suspense boundary or a dynamic import. If a user lands on a product page and the "Add to cart" button requires a lazy loaded module to function, the first click after the page loads will have a delay while that module downloads. The user perceives a broken or slow button. This is worse than including the module in the initial bundle would have been. The rule I apply is: lazy load for route boundaries and for features that require explicit user navigation to reach. Do not lazy load for components that are part of the core interaction flow of any given page.
Common mistakes
Lazy loading the hero image. The most common lazy loading mistake, and the one with the biggest impact. If
loading="lazy"is on the largest above fold image on any page, remove it. Addfetchpriority="high"in its place to tell the browser to prioritize it.Not setting width and height on lazy loaded images. An img element without explicit dimensions causes Cumulative Layout Shift when it loads. Set width and height on every img, including lazy loaded ones, so the browser can reserve the correct space before the image loads.
Lazy loading JavaScript for interactive features that users access immediately. If clicking a button triggers a lazy import, the first click has additional latency from the download. Only lazy load features that users navigate to, not features that respond to immediate user actions.
Using a JavaScript intersection observer library where native support already exists. The
loading="lazy"attribute has full modern browser support. A JavaScript library that implements the same behavior adds bundle weight and complexity for no benefit over the native attribute.Not auditing which components are behind a lazy boundary in production. As codebases grow, it is easy to lose track of which components are lazy loaded and which are not. Periodically audit the lazy boundaries in the codebase and verify that the right components are deferred and the right ones are included eagerly.
Where to start
Audit every img element on your three most trafficked pages. Verify that the LCP image has no
loadingattribute or explicitly hasloading="eager". Verify that all below fold images haveloading="lazy". Check that all images have explicit width and height attributes.Check your JavaScript bundle for code splitting at the route level. Use your bundler's bundle analyzer (webpack-bundle-analyzer or Vite's rollup-plugin-visualizer) to see which modules are in the initial bundle. Identify route specific code that could be split into separate chunks.
Run Lighthouse before and after each lazy loading change. Measure LCP, CLS, and INP before applying lazy loading to any element. Verify that the change improves the relevant metric before shipping it.
FAQ
Frequently asked
- Which images should be lazy loaded and which should not?
- How does React.lazy work for code splitting?
- What is the relationship between lazy loading and Core Web Vitals?
- What is the loading=lazy attribute and what browsers support it?
- When does lazy loading JavaScript hurt more than it helps?
Author
Closing note from the author
I keep these closing notes short on purpose. Most engineers writing about this topic are not the engineer you want to hire. I might be. Yashveer Singh, founder of Yashveer Labs. The contact channel is Instagram. The proof is the portfolio. The standard is in the work. If we are aligned, you will know within five minutes of the first message.