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.
Written by Yashveer Singh, founder of Yashveer Labs.
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. - Route-level code splitting 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. Route-level code splitting 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 and highest-impact lazy loading mistake. 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 for native-supported lazy loading. 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 route-level code splitting. 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.
Related reading
Frequently asked
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.
Posts that line up with this one.
- Performance Optimization
Image Optimization at Scale: AVIF, WebP, Responsive Images
Images are the largest contributor to page weight on most web products. Here is the format selection, responsive image, and delivery strategy that cuts load time without manual work.
- Performance Optimization
INP: The New Core Web Vital Most Teams Are Failing
Interaction to Next Paint replaced First Input Delay in 2024 and it is harder to pass. Most teams have not caught up. Here is what INP measures, why it matters, and how to fix the common failure patterns.
- Performance Optimization
Largest Contentful Paint: The Metric That Changes Conversions
LCP is the Core Web Vital that measures how fast the main content loads. It is also the metric most directly correlated with conversion rate. Here is what causes poor LCP and how to fix it systematically.
- Performance Optimization
Memory Leaks in Long Lived Web Apps
Memory leaks in single-page applications are invisible until the tab slows to a crawl. Here is how they form, how to detect them, and the patterns that eliminate the most common sources in React applications.