懒加载没懒成?Nuxt 动态 import 自动 prefetch 的坑

📅 发布时间:2026/10/7 15:08:31
懒加载没懒成?Nuxt 动态 import 自动 prefetch 的坑
最近一直在做站点的性能优化。毕竟在线工具和博客都跑在阿里云的一台3M小水管机器上换算下来理论峰值 384 KB/s实际也就是 300 KB/s 的水平。用户对速度的感知几乎都集中在首次加载。所以重中之重是给它减负把不需要执行的 JS 从这一步拿掉。毕竟首次加载多下 200 KB用户就得多等大半秒。现在用不上那就等真用到时再下。标准操作就是const{renderLua}awaitimport(~/lib/format-lua)改完后 F12 一看。不对劲。我什么都没点那个触发才下载的 JS 还是被下载了只是 Priority Lowest。背景目前使用版本 Nodev24.18.0、Nuxt4.5.2、pnpm11.15.1Nuxt 项目打包之后代码会被切成很多个.js文件也就是常说的 chunk。一个页面不会用到全部 chunk所以打包器要在 HTML 里留几个标记告诉浏览器「这个文件待会儿要用先去拿来」。负责留标记的是一类link标签常用的有两种linkrelmodulepreloadhref/_nuxt/xxx.jslinkrelprefetchhref/_nuxt/xxx.jsmodulepreload的意思是「这个文件我现在就要用」。浏览器解析到这一行立刻用高优先级去下载跟 CSS、字体抢带宽。prefetch的意思是「这个文件以后可能会用」。浏览器先把当前页面的事干完等网络闲下来再用低优先级慢慢下。两个标签的区别不在「下不下」而在「什么时候下、抢不抢带宽」。两者最终都会把文件拿到本地只是modulepreload要跟首次加载抢带宽prefetch排在队尾。这两个标签基本不用手写。Nuxt 在构建时会读代码里的 import 关系自己决定往 HTML 里塞哪几个。而我在这个模块上写的是await import()既不是静态导入也没手写 prefetch首次加载也用不到它。可generate之后它还是变成了 prefetchlinkrelprefetchhref/_nuxt/Dxxxxxxx.js复现我新建了一个 Nuxt 项目分别把动态导入的几种写法拆成几个独立页面每个页面都配了一个体积相同的200 KB载荷。项目代码放在文章末尾了。页面写法/clean/什么都不导入基线/link-modulepreload/顶层静态import/link-prefetch/link relprefetch/await-import-prefetch/函数内await import()一级/await-import-nested/页面 → 一级 mid → 二级载荷/async-component/defineAsyncComponent无条件渲染/async-component-conditional/defineAsyncComponentv-ifpnpmgen# 生成载荷必须先跑pnpmgeneratepnpmanalyze npx--yeshttp-server .output/public-p6521-c-1实测先看基线。/clean/什么都没导入理论上应该最干净。实测下来它仍然下了两个文件4.0 KB 和 3.7 KBPriority 都是Lowest。这是 Nuxt 内置的 error-404 和 error-500被 prefetch 到了每一个页面上七个页面一个不落。它们是 Nuxt 的固定开销后面所有页面的数字里都包含这 7.7 KB。然后看关键的那一页。/await-import-prefetch/里那个 200 KB 的载荷是这样导入的asyncfunctionload(){constmodawaitimport(~/lib/payload-dynamic)output.valuemod.payloadRender()}它挂在一个按钮上。用户不点这段代码就不会执行。打开页面什么都不点等三秒那个 178 KB 的 chunk 已经在列表里了Type是javascriptPriority是Lowest。我用await import()的本意是「用到才下」。但页面刚一空闲浏览器就把它下完了。点击按钮同一个文件会以另一种身份再出现一次同一个文件Dxxxxxxx.js这次的Type是scriptPriority变成High。这说明 prefetch 并未替代这次加载它只是把文件提前放进了缓存。用户真正触发时浏览器仍会走完整的加载流程。省下的是等待没省下的是流量。异步组件也绕不开那不用await import()改用异步组件呢constPayloadAsyncdefineAsyncComponent(()import(~/components/PayloadAsync.vue))PayloadAsync/实测178 KBType是scriptPriority是High。比 prefetch 更狠。它不是等网络空闲而是和 CSS、字体一起抢带宽。我的理解是这个组件在服务端渲染时确实被渲染了Nuxt 判断它「首次加载就要用」于是用最高优先级预加载省掉客户端水合时再等一次。那给它加个v-if让它不参与初次渲染会怎样PayloadAsyncv-ifshow/Priority从High降到了Lowest。但页面总传输量还是 373 KB和上一个页面一模一样。我没点按钮那 178 KB 已经下完了。点一下按钮它才真正被用上所以v-if只解决了「抢首次加载的带宽」没解决「白下」。异步组件这条路绕不开。三组对照七个页面里有三组可以两两对比。对比出来的结论比单看一页更有意思。一、静态导入 vs 异步组件页面载荷TypePriority/link-modulepreload/178 KBscriptHigh/async-component/178 KBscriptHigh两者情况一样。静态导入走modulepreload理所当然。问题在于后面那一行一个叫「异步」的组件只要它参与了初次渲染Nuxt 就把它当首次加载的资源处理「异步」这两个字等于白叫。二、手写的 prefetch vs 框架注入的 prefetch页面载荷TypePriority/link-prefetch/208 KBjavascriptLowest/await-import-prefetch/178 KBjavascriptLowest这一组验证一件事Nuxt 注入的 prefetch 标签和手写的有没有区别。结论是没有。两者的Type都是javascriptPriority都是Lowest加载时机也一致。也就是说Nuxt 注入的这一行与手写的link relprefetch等价区别只在于前者由构建过程自动完成后者由开发者显式声明。三、一级动态 import vs 条件渲染的异步组件页面载荷TypePriority页面总传输/await-import-prefetch/178 KBjavascriptLowest372 KB/async-component-conditional/178 KBjavascriptLowest373 KB两条路的结果一致。await import()和defineAsyncComponentv-if的区别只在语法层面。只要被导入的模块挂在「一级」结果都是零交互下先下载 178 KB仅优先级为Lowest。更换写法并不能规避。七页放在一起看页面载荷TypePriority页面总传输/clean/———194 KB/link-modulepreload/178 KBscriptHigh372 KB/link-prefetch/208 KBjavascriptLowest402 KB/await-import-prefetch/178 KBjavascriptLowest372 KB/await-import-nested/0.6 KBjavascriptLowest195 KB/async-component/178 KBscriptHigh373 KB/async-component-conditional/178 KBjavascriptLowest373 KB几点结论一级await import()的懒加载不成立。零交互下 178 KB 即被下载仅优先级为Lowest。二级动态导入成立。页面总传输 195 KB与基线基本持平。v-if只改变优先级不改变流量。异步组件无条件渲染时行为等同于静态导入。存在固定开销error-404 与 error-500 两个 chunk 被 prefetch 到全部七个页面每页 7.7 KB。对照引言中那台 3M 带宽的机器178 KB 约合半秒以上的等待7.7 KB 则是每个页面都要重复支付的一次成本。解决方案页面不再直接导入那个 200 KB 的模块而是导入一个很小的中间模块中间模块内部再去导入载荷// 页面asyncfunctionload(){constmidawaitimport(~/lib/nested-mid)output.valueawaitmid.loadLeaf()}// nested-mid.tsexportasyncfunctionloadLeaf(){constmodawaitimport(./payload-nested)returnmod.payloadRender()}同样不做任何操作等待三秒一级那个中间模块0.6 KB仍被 prefetch但二级的 178 KB 连请求都没有发出。点击按钮后才会加载再对比页面总传输量页面总传输/clean/基线194 KB/await-import-nested/195 KB/await-import-prefetch/372 KB二级嵌套的页面只比基线多出 1 KB就是那个中转模块的体积。而一级动态导入的页面多出 178 KB。我目前的处理方式是把体积大的模块下移到二级。中间那层转发模块很小即使被 prefetch 影响也有限。关于这个问题的记录最后我查了一下 GitHub发现这不是新问题#18376 Disableprefetchfor dynamic imports2023-01-20 提出2026-05-05 关闭#14584 optimisations for prefetching chunks2022-08-15 提出2026-09-06 关闭#36391 unified client prefetch scheduler2026-09-23 合并目标版本 5.x#18376 描述的场景和本文遇到的一模一样。上游近期把客户端预取的调度统一到了一套队列#36391但它调整的是预取的秩序不是「要不要预取动态 import」是否影响本文描述的现象尚不确定需要持续观察。在此之前可行的做法仍是自行处理把大体积模块放到第二层动态 import第一层只保留一个转发用的空壳。相关代码相关代码在我的公众号或博客下载