Next.js v12.1 chunks等脚本偶发阻塞渲染原因及解决方法
Next.js v12.1 自托管场景下框架内置脚本偶发渲染阻塞问题解答
一、脚本时而阻塞渲染、时而不阻塞的根本原因
- Next.js v12.1 版本默认的脚本注入逻辑存在设计局限:框架自动生成的核心运行时chunk、三类manifest清单脚本默认以无
defer/async属性的普通<script>标签注入页面,浏览器遇到这类标签时,默认会暂停HTML解析流程,等待脚本下载、执行完成后才继续渲染,这是产生阻塞的基础前提。 - 性能检测工具对渲染阻塞脚本的通用判定规则是:首屏绘制(FCP)前发起网络请求加载、且未标记异步/延迟加载属性的脚本,或执行时阻塞主线程时长超过阈值的脚本,才会被标记为阻塞资源,这是偶发判定差异的核心判断标准。
- 桌面端偶发不触发阻塞告警的原因:测试场景下的缓存状态、网络状态存在波动。如果脚本已经被浏览器强缓存命中,脚本直接从本地内存/磁盘读取,没有网络下载耗时,执行耗时也极短,达不到工具的阻塞判定阈值,就不会被标记;冷加载无缓存时,如果桌面端网络带宽足够高、延迟足够低,脚本下载+执行的总耗时偶尔也会低于判定阈值,就会出现同环境下时而告警、时而正常的现象。
- 移动端几乎每次都触发阻塞告警的原因:移动端普遍网络带宽更低、往返延迟更高,且设备CPU性能弱于桌面端,无缓存场景下脚本下载耗时久,有缓存场景下脚本执行的主线程阻塞时长也更容易超过判定阈值,因此几乎每次测试都会被识别为渲染阻塞资源。
- 额外触发因素:
_buildManifest.js、_ssgManifest.js、_middlewareManifest.js三个清单脚本在v12.1版本中是硬编码注入到文档头部的,完全没有做加载策略优化,是阻塞问题的高发来源。
二、实现这类脚本始终非阻塞加载的配置调整方案
所有调整均基于Next.js v12.1版本的原生能力实现,不需要额外升级大版本:
- 第一步:自定义
pages/_document.js,给所有框架自动注入的核心chunk添加defer属性,禁止使用async(会打乱脚本执行依赖顺序,引发运行时报错)。参考实现代码:
import Document, { Html, Head, Main, NextScript } from 'next/document' class MyDocument extends Document { render() { return ( <Html lang="zh-CN"> <Head /> <body> <Main /> <NextScript scriptLoader={{ ...this.props.scriptLoader, scripts: (this.props.scriptLoader?.scripts || []).map(script => ({ ...script, props: { ...script.props, defer: true, async: false } })) }} /> </body> </Html> ) } } export default MyDocument
- 第二步:通过自托管服务的响应拦截逻辑,处理三个硬编码注入的manifest脚本:移除Next.js默认注入在头部的无属性manifest标签,改为在body末尾注入带
defer属性的对应脚本。Node.js自托管场景的参考拦截逻辑:
// 托管服务响应拦截中间件示例 const interceptRenderResponse = (app, buildId) => { const originalSend = app.response.send app.response.send = function (htmlContent) { if (typeof htmlContent === 'string' && htmlContent.includes('_next/static')) { // 移除默认注入的无属性manifest脚本 htmlContent = htmlContent.replace( /<script src="\/_next\/static\/[^"]+\/_(build|ssg|middleware)Manifest\.js"><\/script>/g, '' ) // 插入带defer属性的manifest脚本,保证执行顺序 const manifestTags = ` <script defer src="/_next/static/${buildId}/_buildManifest.js"></script> <script defer src="/_next/static/${buildId}/_ssgManifest.js"></script> <script defer src="/_next/static/${buildId}/_middlewareManifest.js"></script> ` htmlContent = htmlContent.replace('</body>', `${manifestTags}</body>`) } return originalSend.call(this, htmlContent) } }
- 第三步:开启v12.1版本内置的生产优化项,降低脚本本身的体积和执行耗时:
- 在
next.config.js中开启swcMinify: true,使用SWC替代Terser做代码压缩,平均可减小chunk体积15%-20% - 开启
experimental.optimizeCss: true,避免脚本等待CSS加载产生额外阻塞 - 自托管场景可配置
output: 'standalone',剔除不必要的运行时代码注入
- 在
- 第四步:给
/_next/static/路径下的所有静态资源配置强缓存响应头,设置Cache-Control: public, max-age=31536000, immutable,用户二次访问时直接从本地缓存读取脚本,完全消除网络下载阶段的耗时,进一步降低阻塞概率。
注意:
defer属性会保证脚本在整个HTML文档解析完成后、DOMContentLoaded事件触发前按标签顺序执行,完全适配Next.js的chunk依赖顺序,不会引发运行时错误,是这类框架脚本非阻塞加载的最优选择。
内容的提问来源于stack exchange,提问作者Noitidart
相关产品推荐
相关产品推荐

