Code splitting and lazy loading cut the JavaScript, CSS and media a visitor must download before a page becomes usable. A store, a dashboard or a marketing site can ship one enormous bundle that parses on every visit, or it can send a small first view and load the chart, the map, the editor and the off-screen images only when someone actually needs them.
Contents
At CodeWiz we audit what the browser downloads, split bundles by route and by feature, lazy-load media and third-party scripts, and put a size budget in CI so the next feature does not quietly undo the gain. The work covers Vite, webpack, Rollup, Next.js, Nuxt, plain PHP templates and WordPress, including shops where a handful of plugins enqueue scripts on every URL.
This service is for teams whose Lighthouse score drops every time a widget is added, for stores whose mobile visitors leave during a long white screen, and for products that feel slow to click even though the server responds quickly. If code splitting and lazy loading are tied to a budget and to field data, the page stays fast as the product grows.

Bundle Optimization: Ship Less JavaScript on the First View
Bundle optimization is the work of finding what landed in the JavaScript file and removing, replacing or postponing whatever the first view does not need. A bundler such as Vite, Rollup or webpack walks the import graph and emits one or more files. Tree-shaking drops unused exports when the code is real ESM and the sideEffects flag is honest. It cannot save you from a root import of an icon pack, a date library or a UI kit that re-exports everything from a single barrel file.
What a heavy bundle costs the business
Download time is only the first cost. The browser still has to parse and compile that JavaScript on the main thread, and on a mid-range phone that work delays the first paint and the first click. Google’s Core Web Vitals treat Largest Contentful Paint above 2.5 seconds and Interaction to Next Paint above 200 milliseconds as poor. Both get worse when a page boots hundreds of kilobytes of unused code.
How we optimize a bundle
- A visual map of the bundle with
rollup-plugin-visualizer, webpack-bundle-analyzer or the Next.js analyzer, so the heaviest modules are obvious before anyone rewrites a component. - Direct imports instead of barrel files, and libraries swapped for smaller ones where the API allows it: a locale-sized date helper instead of a general-purpose library, individual icons instead of the whole set.
- A browserslist that matches the browsers you actually support, so modern visitors are not downloading polyfills for engines you no longer serve.
- Route-level chunks for each major screen, plus a shared vendor chunk that changes only when dependencies change, so returning visitors keep a long-lived cache.
- A CI check with size-limit, bundlesize or a Lighthouse budget. A pull request that pushes the initial JavaScript past the agreed limit fails before it is merged.
| Approach | What it splits | When it loads | Watch out for |
|---|---|---|---|
| Route chunks | Each page or layout | On navigation | A blank gap if the next view has no placeholder |
| Component import | A heavy widget | On click, idle time or visibility | A delay the first time that widget is used |
| Vendor chunk | Shared libraries | With the first page that needs them | One dependency bump can invalidate a large cached file |
| CSS split | Styles for a route | With that route | A flash of unstyled content if critical CSS is missing |
| Delayed third parties | Chat, tags, video players | After consent or interaction | Keep the tags the business truly measures |
The dynamic import() operator is the language feature behind most of these splits. The bundler turns each import into its own file; we decide which imports belong on the first screen.
Lazy Loading for Images, Video and Off-Screen Content
Lazy loading defers work the visitor cannot see yet. Applied to the hero image, it makes Largest Contentful Paint worse. Applied to everything below the fold, to embeds and to scripts that wait for a click, it is one of the cheapest speed improvements on an existing site.
Images, video and embeds
- The LCP image is eager: explicit width and height, a modern format such as AVIF or WebP, a responsive
srcset, andfetchpriority="high"so the browser fetches it first. - Images below the fold use
loading="lazy"anddecoding="async", with dimensions set so the layout does not jump when they arrive. - Video uses
preload="none"and a poster image. A multi-megabyte file never starts downloading on a page the visitor may never play. - YouTube, maps and other iframes stay a static poster until a click, then the real iframe mounts.
loading="lazy"covers the embeds that must be present earlier. - Long articles can use
content-visibility: autoon sections far down the page, so the browser skips layout work until the reader gets there.
<img src="hero.avif" alt="Product on a table" width="1600" height="900" fetchpriority="high">
<img src="gallery.avif" alt="Detail of the same product" width="800" height="600" loading="lazy" decoding="async">Widgets and third-party scripts
Chat widgets, A/B testing tags, help desks and social embeds often cost more main-thread time than the page itself. We load them after the first interaction, after consent, or when their container nears the viewport. On WordPress and WooCommerce the same idea applies to plugins: scripts and styles are enqueued only on the templates that use them, and the cart, checkout and account pages keep the assets they actually need. A CDN and cache layer still matter for the HTML and images around that JavaScript — the same edge setup we describe in our Cloudflare configuration guide.

Dynamic Imports That Load Code at the Moment of Use
A dynamic import is a function call that returns a promise and tells the bundler to emit a separate chunk. The editor, the map, the rich-text toolbar, the admin chart and the locale file for a language the visitor has not chosen stay out of the initial download. The first time they are needed, the browser fetches that one file.
document.querySelector('#open-chart').addEventListener('click', async () => {
const { renderChart } = await import('./chart.js');
renderChart(document.querySelector('#chart'), window.reportData);
});- React:
lazy()andSuspensearound a component that has a default export, with a fallback that reserves space so the page does not jump. - Vue and Nuxt:
defineAsyncComponent, or Nuxt’s lazy component prefixes, for panels that open on demand. - Next.js: route segments already split the App Router.
next/dynamiccovers client-only widgets such as maps that should not run during server rendering. - Vite and webpack: a plain
import()is enough.manualChunkskeeps React, Vue or another stable runtime in its own cached file. Prefetch belongs on the one or two routes a visitor is likely to open next, not on every chunk.
We name the fallback, the error state and the timeout. A chunk that fails on a flaky mobile network should offer a retry, and a loading skeleton should match the widget’s height.
Performance Monitoring for Code Splitting and Lazy Loading
A split that nobody measures will drift. New dependencies, a new plugin and a new marketing tag all add weight, and the regression shows up in field data weeks later. Performance monitoring closes that loop with a lab check on every change and field data from real devices.
| Signal | Target we watch | Where it comes from |
|---|---|---|
| JavaScript on first load | Under an agreed kilobyte budget | Bundle analyzer and a CI size check |
| Largest Contentful Paint | Under 2.5 seconds in the field | web-vitals, CrUX, Lighthouse |
| Interaction to Next Paint | Under 200 milliseconds | web-vitals and a performance trace |
| Main-thread long tasks | Fewer tasks over 50 milliseconds at startup | Chrome performance panel |
| Chunk caching | Stable hashes for code that did not change | CDN logs and Cloudflare analytics |
- Lighthouse CI runs on pull requests against a mobile profile, so a heavier page is visible before release.
- The
web-vitalslibrary reports LCP, INP and CLS from real sessions into the analytics tool you already use. - Chrome UX Report data, once the origin has enough traffic, shows whether the lab win survived real phones and networks.
- Source maps go to the error tracker, not to the public as extra download weight.
- A short note in the repository records the budget and which routes are allowed to be heavier, such as a logged-in editor.
Lab scores and field data answer different questions, and we keep both. The same measurement habit is how we approach 100/100 page vitals, with caching in front of the HTML when the stack calls for it — including Varnish in front of a dynamic site. Google’s own Core Web Vitals documentation is the reference for the thresholds above.
Route Splits, Shared Chunks and the Point of Diminishing Returns
More chunks are not automatically a faster site. Each file has a request, and a waterfall of tiny chunks can lose the time you saved by shrinking the first one. HTTP/2 and HTTP/3 make many small files cheaper than they were on HTTP/1.1; they do not make them free. We group code the way visitors actually move through the product.
- One chunk per route or layout, not one chunk per button.
- Libraries used on almost every page live in a shared chunk with a stable hash.
- Code used by a single admin screen is loaded only inside that screen.
- Critical CSS for the first view is inlined or loaded immediately; the rest can follow the route.
- Prefetch runs on hover or on idle for the next likely page, and only after the current page is idle.
Server-rendered and static pages follow the same rule. The HTML for the first screen should already contain the content, and later JavaScript should enhance it.
Common Code Splitting and Lazy Loading Mistakes
- Lazy-loading the hero image, which delays Largest Contentful Paint on the exact element Google measures.
- Importing an entire icon or component library from its package root, which defeats tree-shaking.
- Calling
import()on every small component, then waiting on a chain of requests at startup. - Shipping polyfills and legacy bundles to modern browsers because browserslist was left at its default.
- Forgetting a reserved-height fallback, so content jumps when a lazy image or chunk arrives and Cumulative Layout Shift suffers.
- Treating a green Lighthouse run on a desktop as proof, while field data on mobile still shows a slow interaction.
- Letting WordPress plugins print their scripts in the footer of every template, including pages that never use the feature.
What Drives the Cost of Bundle and Loading Work
We estimate after looking at a production build, not from a guess. The cost follows the shape of the front end:
- A single Vite or Next.js app with a clear route map is a smaller job than a site assembled from several plugins and a page builder.
- The number of templates that each load their own scripts, especially on WordPress, WooCommerce and similar stacks.
- How many third-party tags marketing needs to keep, and which of them can wait for consent or a click.
- Whether design changes are in scope, or the work is limited to loading behaviour and build output.
- The monitoring setup: a one-off audit, or a budget that stays in CI and a field report that stays on afterwards.

How We Work
- Discovery: we measure the current build, the templates and the field vitals, and list the modules and tags that dominate the first load.
- Plan: we agree a budget for the initial JavaScript, which routes split, which images stay eager and which third parties wait.
- Build: we change imports, add dynamic splits, fix image markup and wire the bundle budget into CI.
- Test: we compare before and after on a mobile profile and on a real phone, including a slow network and a failed chunk.
- Launch: we release behind the existing cache layer, watch field LCP and INP, and keep a quick rollback of the build.
- Support: we keep the budget in place as new features land, and revisit chunks when a dependency update moves the numbers.
Why CodeWiz
- Full-stack performance work: bundle splits, image markup, Varnish and Cloudflare sit with the same team that builds the application.
- Open-source tooling: Vite, Rollup, webpack, Lighthouse and web-vitals, without a proprietary performance suite.
- Advice matched to the stack: a WordPress shop and a Next.js product get different splits, because their bottlenecks differ.
- Technology in service of the business: we cut the bytes that delay a purchase or a signup, and we leave harmless code alone.
- Fair pricing: premium development without premium prices, from Banja Luka, working remotely with clients locally and internationally. More about CodeWiz.
Frequently Asked Questions
Will code splitting improve Core Web Vitals?
It improves the vitals that JavaScript and images actually affect. Less script on the first view usually helps LCP and INP, and correct image dimensions protect CLS. The gain shows up when the removed code was on the critical path. We measure before and after rather than promising a specific score.
What is the difference between code splitting and lazy loading?
Code splitting breaks the build into smaller files. Lazy loading decides when those files, and when images or embeds, are requested. You can split a bundle and still download every chunk immediately. The useful combination is a small initial file plus a clear rule for everything else.
Does lazy loading hurt SEO?
Lazy loading of below-the-fold images is normal and expected. The failure mode is hiding primary content behind JavaScript that search engines or users must wait on, or delaying the image that forms the largest paint. We keep the first screen in the HTML and lazy-load what comes after it.
Can you do this on an existing WordPress or React site?
Yes. React, Vue, Next.js and Nuxt projects usually need import changes and a budget in CI. WordPress and WooCommerce projects usually need a per-template script audit, image markup fixes and a delay on third-party tags. A rewrite is rarely required.
How long does bundle optimization take?
A focused pass on one application — analyzer, the worst offenders, image lazy loading and a CI budget — is often a short engagement. A large multi-theme WordPress site or a design-system migration with many routes takes longer, mostly because each template has to be checked. You get an estimate after we see a production build.
Which bundler do you recommend?
For new application front ends we usually use Vite. Existing webpack, Next.js or Nuxt builds stay on their toolchain unless the build itself is the problem. The splitting rules matter more than the brand of the bundler, and we will say so when a migration would cost more than it returns.
Ready to Ship a Smaller First Load?
Send us a production URL or a build, and we will show you what the first view is downloading and which splits are worth doing. Contact us to talk through the project, or see our code splitting and lazy loading services.