Skip to content

Next.js Bundle Analyzer in 16.4: The Import That Defeats Lazy Load

Next.js Bundle Analyzer in 16.4: The Import That Defeats Lazy Load

Confession: bundle analyzers are the tool I know I should open more often and mostly don’t. The usual finding is something small and slightly embarrassing, like a date library imported for one format() call and sitting in the initial chunk of a page with no dates on it. You fix it, feel smug for a day, and forget to check again until someone complains about load time.

So when the Next.js team published Ship less JavaScript in Next.js with your AI agent on 28 October, I read it twice. Short version for the impatient: Next.js 16.4 adds an agent skill and a better next analyze, and the most useful idea in the post is a boring one about imports that quietly undo each other. I haven’t run the skill on a production app yet, so this isn’t a review. It’s what I would check first, and why I’m more interested in the analyzer than in the agent bolted onto it.

The lazy import that never deferred anything

Here is the example from the Next.js post, nearly verbatim in spirit. You have a heavy component and you want it out of the initial bundle, so you wrap it:

// HeavyComponentLoader.js
'use client';
import { lazy } from 'react';

export const HeavyComponentLoader = lazy(() => import('./HeavyComponent'));

That looks right. React’s lazy documentation says the code for a lazy component isn’t loaded until it first renders, and the Next.js lazy loading guide says much the same about next/dynamic. Both are true. The catch is that the bundler works on the whole module graph, not on your intentions. If any other module in the same bundle does this:

// Toolbar.js
import HeavyComponent from './HeavyComponent';

then HeavyComponent is part of the initial chunk anyway, and your lazy wrapper becomes a decoration. One static import, three folders away, written by someone who never saw your loader. The bundle doesn’t warn you. Lighthouse just gets a little worse and nobody connects it to a pull request from last month.

Picture a chart library that is lazy loaded on a reports page, which feels like a good job done. Then someone adds a small summary card to the home page that imports one helper from the same chart file, and the whole library moves into the shared chunk. You’d stare at the lazy call for an hour, because the lazy call is fine. The bug is somewhere else entirely. I haven’t been bitten by this exact case on a client project, but it’s the shape of bug I’d expect to find in any app with more than one contributor.

The old way to find this was to open a treemap, spot the big rectangle, and then grep the codebase for every place that imports it. It works. It’s also slow, and you only do it when someone is already complaining about load time.

What next analyze gives you without any agent

Before the agent part, the human tooling. In 16.1 the team shipped the first Turbopack Bundle Analyzer, and 16.4 adds a few things that matter more than the headline. You run next analyze in your app directory, and you now land on a route summary that lists your largest client routes. Open one and there’s a table you can sort by size, with each source marked as initial load, behind an async boundary, or both.

That last label is the one I care about. A module marked “Initial + async paths” is exactly the lazy-import bug above: it’s loaded lazily from one place and eagerly from another. In the post’s illustrative blog app, a search component sits at 18 kB with that label. You can see the problem in one row, without reading a treemap.

Two more features I’d use right away. The analyzer now saves a snapshot of each analysis automatically, and you can diff two snapshots, which gives you a before and after for any change. The post’s example goes from 123 kB to 81 kB on one route, with a per-file table of what moved. And there’s a filter for the critical render path, so you can hide async dependencies and look only at what blocks first paint.

I’ll be blunt about one limitation. All of this needs Turbopack, because the analyzer relies on its module graph. If you’re on 16 or later, that’s the default and you won’t think about it. If you pinned webpack for a plugin that doesn’t work elsewhere, you stay with the older webpack bundle analyzer and none of this applies to you yet.

What the agent skill adds

The skill is installed with npx skills add vercel/next.js --skill next-bundle-optimizer and run with /next-bundle-optimizer. According to the post, it exports saved analyses as JSON, rebuilds the module and chunk graphs, finds large groups of connected modules, and uses a minimum cut to pick a small set of import boundaries that would defer them. Then it checks those proposals against your source code and how the components are used.

The minimum cut is the clever part, and it’s a good fit for the problem. “Which handful of imports, if I changed them, would let this whole cluster load later?” is a graph question. It’s the question I answer badly by hand, because I follow the first import chain I find and stop. A tool that looks at every chain at once will beat me on that, and I have no pride about it.

By default it only reports. You have to run /next-bundle-optimizer fix to have it apply changes, re-run the analysis, and compare snapshots. I like that default. It means the first run on a client codebase is read-only, and I can bill the findings as an audit before anyone touches a file.

The numbers in the post are large. Across v0 and vercel.com, the team says agents cut nearly two megabytes of compressed JavaScript, with individual wins like 150 KB from an experimental editor loaded on nearly every page, and 255 KB on the Vercel dashboard from markdown rendering and chart drawing that shipped before the agent panel was even open. Those are self-reported results on Vercel’s own apps, tuned by the people who wrote the tool. Treat them as proof the approach works, not as a forecast for your app. A typical agency site has fewer megabytes to find. I’d be happy with three or four wins in the 20 to 80 KB range.

Where I would not trust it blindly

Deferring code isn’t free, and this is where I’d push back on the “let the agent open a pull request” workflow. When you move a 150 KB editor behind a dynamic import, you haven’t removed the cost. You’ve moved it to the moment the user needs the editor. If that moment is a click, the first click now waits on a network request. On a fast connection you won’t notice. On a train in poor coverage, the button looks broken.

So every proposed boundary needs a question: what does the user see while this loads? A spinner, a layout jump, a blank panel? The post says the agent checks how components are used interactively, and I believe it tries. But a model reading source can’t feel a 400 ms gap on a mid-range phone. You can.

My review checklist for each agent change, in order:

  1. Is the deferred component above the fold or behind an interaction?
  2. Does the loading state match the final layout, so nothing shifts?
  3. Did INP get worse on the page, measured on a preview deployment rather than on my laptop?
  4. Is the saving big enough to justify the extra moving part?

The Vercel team says they verified each fix on a preview deployment before shipping, which is the right habit. Copy it. I’d also keep pull requests one change each, the way the post suggests, because a single combined “optimize bundles” diff is impossible to bisect when INP regresses.

If you are also deciding whether Next.js is the right base for a project in the first place, I went through that trade-off in my Next.js vs Vite post. The bundle tooling above is one more thing that tilts the answer, mostly for teams already on Turbopack.

A routine I would run this week

You don’t need the agent to get most of the value. Here’s the sequence I’m going to use on my next client audit, which is the kind of work I describe on my portfolio:

# on a branch, after upgrading to Next.js 16.4
npx next analyze

# optional: read-only report from the agent skill
npx skills add vercel/next.js --skill next-bundle-optimizer
# then run /next-bundle-optimizer in your agent (no "fix" argument)

# export the graph if you want to poke at it yourself
npx next analyze export

Start with the route summary and write down the three largest client routes. In each, sort by size and look for two things: any source labeled “Initial + async paths”, and any package you can’t explain. A date library on a page with no dates counts. Then pick exactly one finding, fix it by hand, take a new snapshot, and read the diff. If the diff shows the number you expected, you understand your bundle. If it doesn’t, the import graph is teaching you something, and that’s the useful part.

Only after that would I run the agent in report mode and compare its list with mine. Where we disagree, I want to know why before I apply anything.

This week, run next analyze on your biggest route and write down the single largest file in the initial load. If you can’t say why it’s there, trace its imports until you can.