{"id":814,"date":"2026-10-30T13:00:35","date_gmt":"2026-10-30T13:00:35","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/nextjs-bundle-analyzer-16-4-the-static-import-that-defeats-lazy-load\/"},"modified":"2026-10-30T13:00:35","modified_gmt":"2026-10-30T13:00:35","slug":"nextjs-bundle-analyzer-16-4-the-static-import-that-defeats-lazy-load","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/nextjs-bundle-analyzer-16-4-the-static-import-that-defeats-lazy-load\/","title":{"rendered":"Next.js Bundle Analyzer in 16.4: The Import That Defeats Lazy Load"},"content":{"rendered":"<p>Confession: bundle analyzers are the tool I know I should open more often and mostly don&rsquo;t. The usual finding is something small and slightly embarrassing, like a date library imported for one <code>format()<\/code> 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.<\/p>\n<p>So when the Next.js team published <a href=\"https:\/\/nextjs.org\/blog\/ship-less-javascript-with-ai\" rel=\"nofollow noopener\" target=\"_blank\">Ship less JavaScript in Next.js with your AI agent<\/a> on 28 October, I read it twice. Short version for the impatient: Next.js 16.4 adds an agent skill and a better <code>next analyze<\/code>, and the most useful idea in the post is a boring one about imports that quietly undo each other. I haven&rsquo;t run the skill on a production app yet, so this isn&rsquo;t a review. It&rsquo;s what I would check first, and why I&rsquo;m more interested in the analyzer than in the agent bolted onto it.<\/p>\n<h2 id=\"the-lazy-import-that-never-deferred-anything\">The lazy import that never deferred anything<\/h2>\n<p>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:<\/p>\n<pre><code class=\"language-jsx\">\/\/ HeavyComponentLoader.js\n'use client';\nimport { lazy } from 'react';\n\nexport const HeavyComponentLoader = lazy(() =&gt; import('.\/HeavyComponent'));\n<\/code><\/pre>\n<p>That looks right. React&rsquo;s <a href=\"https:\/\/react.dev\/reference\/react\/lazy\" rel=\"nofollow noopener\" target=\"_blank\">lazy<\/a> documentation says the code for a lazy component isn&rsquo;t loaded until it first renders, and the Next.js <a href=\"https:\/\/nextjs.org\/docs\/app\/guides\/lazy-loading\" rel=\"nofollow noopener\" target=\"_blank\">lazy loading guide<\/a> says much the same about <code>next\/dynamic<\/code>. 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:<\/p>\n<pre><code class=\"language-jsx\">\/\/ Toolbar.js\nimport HeavyComponent from '.\/HeavyComponent';\n<\/code><\/pre>\n<p>then <code>HeavyComponent<\/code> 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&rsquo;t warn you. Lighthouse just gets a little worse and nobody connects it to a pull request from last month.<\/p>\n<p>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&rsquo;d stare at the lazy call for an hour, because the lazy call is fine. The bug is somewhere else entirely. I haven&rsquo;t been bitten by this exact case on a client project, but it&rsquo;s the shape of bug I&rsquo;d expect to find in any app with more than one contributor.<\/p>\n<p>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&rsquo;s also slow, and you only do it when someone is already complaining about load time.<\/p>\n<h2 id=\"what-next-analyze-gives-you-without-any-agent\">What next analyze gives you without any agent<\/h2>\n<p>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 <code>next analyze<\/code> in your app directory, and you now land on a route summary that lists your largest client routes. Open one and there&rsquo;s a table you can sort by size, with each source marked as initial load, behind an async boundary, or both.<\/p>\n<p>That last label is the one I care about. A module marked &ldquo;Initial + async paths&rdquo; is exactly the lazy-import bug above: it&rsquo;s loaded lazily from one place and eagerly from another. In the post&rsquo;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.<\/p>\n<p>Two more features I&rsquo;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&rsquo;s example goes from 123 kB to 81 kB on one route, with a per-file table of what moved. And there&rsquo;s a filter for the critical render path, so you can hide async dependencies and look only at what blocks first paint.<\/p>\n<p>I&rsquo;ll be blunt about one limitation. All of this needs Turbopack, because the analyzer relies on its module graph. If you&rsquo;re on 16 or later, that&rsquo;s the default and you won&rsquo;t think about it. If you pinned webpack for a plugin that doesn&rsquo;t work elsewhere, you stay with the older <a href=\"https:\/\/github.com\/webpack-contrib\/webpack-bundle-analyzer\" rel=\"nofollow noopener\" target=\"_blank\">webpack bundle analyzer<\/a> and none of this applies to you yet.<\/p>\n<h2 id=\"what-the-agent-skill-adds\">What the agent skill adds<\/h2>\n<p>The skill is installed with <code>npx skills add vercel\/next.js --skill next-bundle-optimizer<\/code> and run with <code>\/next-bundle-optimizer<\/code>. 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.<\/p>\n<p>The minimum cut is the clever part, and it&rsquo;s a good fit for the problem. &ldquo;Which handful of imports, if I changed them, would let this whole cluster load later?&rdquo; is a graph question. It&rsquo;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.<\/p>\n<p>By default it only reports. You have to run <code>\/next-bundle-optimizer fix<\/code> 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.<\/p>\n<p>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&rsquo;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&rsquo;d be happy with three or four wins in the 20 to 80 KB range.<\/p>\n<h2 id=\"where-i-would-not-trust-it-blindly\">Where I would not trust it blindly<\/h2>\n<p>Deferring code isn&rsquo;t free, and this is where I&rsquo;d push back on the &ldquo;let the agent open a pull request&rdquo; workflow. When you move a 150 KB editor behind a dynamic import, you haven&rsquo;t removed the cost. You&rsquo;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&rsquo;t notice. On a train in poor coverage, the button looks broken.<\/p>\n<p>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&rsquo;t feel a 400 ms gap on a mid-range phone. You can.<\/p>\n<p>My review checklist for each agent change, in order:<\/p>\n<ol>\n<li>Is the deferred component above the fold or behind an interaction?<\/li>\n<li>Does the loading state match the final layout, so nothing shifts?<\/li>\n<li>Did INP get worse on the page, measured on a preview deployment rather than on my laptop?<\/li>\n<li>Is the saving big enough to justify the extra moving part?<\/li>\n<\/ol>\n<p>The Vercel team says they verified each fix on a preview deployment before shipping, which is the right habit. Copy it. I&rsquo;d also keep pull requests one change each, the way the post suggests, because a single combined &ldquo;optimize bundles&rdquo; diff is impossible to bisect when INP regresses.<\/p>\n<p>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 <a href=\"https:\/\/abrarqasim.com\/blog\/nextjs-vs-vite-vinext-1-0-the-use-cache-gap-i-would-check-first\" rel=\"noopener\">Next.js vs Vite post<\/a>. The bundle tooling above is one more thing that tilts the answer, mostly for teams already on Turbopack.<\/p>\n<h2 id=\"a-routine-i-would-run-this-week\">A routine I would run this week<\/h2>\n<p>You don&rsquo;t need the agent to get most of the value. Here&rsquo;s the sequence I&rsquo;m going to use on my next client audit, which is the kind of work I describe on <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">my portfolio<\/a>:<\/p>\n<pre><code class=\"language-bash\"># on a branch, after upgrading to Next.js 16.4\nnpx next analyze\n\n# optional: read-only report from the agent skill\nnpx skills add vercel\/next.js --skill next-bundle-optimizer\n# then run \/next-bundle-optimizer in your agent (no &quot;fix&quot; argument)\n\n# export the graph if you want to poke at it yourself\nnpx next analyze export\n<\/code><\/pre>\n<p>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 &ldquo;Initial + async paths&rdquo;, and any package you can&rsquo;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&rsquo;t, the import graph is teaching you something, and that&rsquo;s the useful part.<\/p>\n<p>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.<\/p>\n<p>This week, run <code>next analyze<\/code> on your biggest route and write down the single largest file in the initial load. If you can&rsquo;t say why it&rsquo;s there, trace its imports until you can.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Next.js 16.4 adds a smarter bundle analyzer and an agent skill. Here is the lazy import trap it finds, what I would check first, and a routine to run this week.<\/p>\n","protected":false},"author":2,"featured_media":813,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Next.js 16.4 adds a smarter bundle analyzer and an agent skill. Here is the lazy import trap it finds, what I would check first, and a routine to run this week.","rank_math_focus_keyword":"webpack bundle analyzer","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[35],"tags":[192,61,19,41,738],"class_list":["post-814","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-web-development","tag-bundle-size","tag-nextjs","tag-performance","tag-react","tag-turbopack"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/814","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=814"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/814\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/813"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=814"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=814"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=814"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}