今年面试季,我旁听了好几轮前端岗位的现场考察。首屏优化几乎是必考题,但候选人的表现两极分化很严重。有一类候选人的开场白特别典型:「我用 webpack 做了拆包,把首屏 JS 从 2MB 砍到了 500KB。」然后开始流畅地背 SplitChunks 的参数。等他说完,面试官追问了一句:「拆完之后,你的 LCP 从多少变成了多少?你怎么在真实用户环境里确认这次拆包真的有效?如果用户从 5G 切到 4G,或者从旗舰机换到中低端 Android,你的结论还成立吗?」多数时候,对面会一下子安静下来。
这个画面的背后,其实是首屏优化考点的一次重要迁移。2026 年的前端面试,考的不是「你会不会拆包」这个单一技巧,而是你是否拥有一个「资源加载 → 渲染架构 → 可观测性」的完整闭环。拆包只是资源加载这一层里最基础的动作,它解决的是传输字节数和缓存命中率的问题,但首屏体验的瓶颈往往更复杂:可能是 HTML 内容到达太晚,可能是 JS 执行阻塞了渲染,可能是数据请求串行太久,也可能只是你根本看不见真实用户现场到底发生了什么。这篇文章不写面试速成话术,我想把首屏优化为什么从「拆包题」变成了「系统博弈题」这件事讲透,顺便给出一套可复用的分析和回答框架。
1. 拆包为什么不再是「万能钥匙」:首屏优化已经换了考法
1.1 三个时代:从体积、拆包到系统博弈
国内前端社区对首屏优化的理解,大致经历过三个清晰的阶段。
第一个阶段是「体积时代」。主流做法是尽可能压缩 JS、CSS,合并请求数,把资源打包成尽量少的文件。当时 HTTP/1.1 并发受限,减少请求本身就是优化,所以 sprite 图、combo 接口、单文件 bundle 都很流行。这个阶段的核心思路只有一个:把首屏需要传输的字节数降下来。
第二个阶段是「拆包时代」。webpack 的 code splitting、动态 import、SplitChunks 和 tree shaking 成熟之后,从「一个 bundle」进化到「多个 chunk」成了标配。常见的姿势是:把框架运行时抽成 vendor chunk,把路由页面拆成按需加载,把公共依赖单独缓存,配合 CDN 让二次访问命中缓存。这个阶段解决的是「首屏需要的资源更少、非首屏资源延后加载」。到目前为止,「会拆包」依然是前端工程化的基本功。
但如果你只停留在第二个阶段,在 2026 年的面试里确实容易露怯。原因在于:拆包操作发生在「网络传输」这一层,而真实的首屏体验并不只是传输问题。一个典型的 SPA,哪怕首屏 JS 被拆得再干净,只要它仍然依赖「完整 HTML 下载 → JS 下载 → JS 解析执行 → 发起数据请求 → 等待接口返回 → 渲染出第一帧内容」这条串行关键路径,用户就得在浏览器里等一条很长的链路。这时候,拆包做得再极致,LCP 也很难有本质变化。
第三个阶段,可以称为「系统时代」。它的标志是,大家意识到首屏优化是一个由资源加载、渲染架构、可观测性三个子系统构成的闭环。资源加载层决定「东西多久能到」;渲染架构层决定「内容什么时候能看见、什么时候能交互」;可观测性层决定「你是怎么知道上面两层出了问题的」。三层之间互相制约,缺一环都不完整。
1.2 面试官不是在考操作,是在考「决策路径」
面试官追问「拆包之后 LCP 变了多少」,真正想听的并不是一个精确数字,而是你有没有完整的决策路径。
只会背技巧的候选人,回答结构通常是:main.js 太大 → 拆 chunk → 首屏只加载需要的 → 结束。这个思路的问题在于,它把优化理解成了一个单点动作,没有定义问题,也没有验证结果。
有系统思维的候选人,回答是分层的:
- 先定义指标。首屏优化到底优化哪个指标?LCP、FCP、INP、TTI 还是白屏时间?不同指标背后的瓶颈来源不一样。
- 再诊断瓶颈。打开 Network 瀑布图,看是请求太多、传输太大、执行太长、渲染被阻塞,还是接口太慢。
- 再选择手段。传输问题用压缩、拆包、CDN、图片格式;渲染问题用 SSR、预渲染、hydration 优化、骨架屏;执行问题用减少 JS 总量、拆分长任务、延后非关键脚本。
- 最后验证效果。用真实用户监控数据对比上线前后的分位数分布,而不是只看本地 DevTools。
这四步加起来,才是「系统博弈」的含义。你要做的不是一道「你用什么工具」的操作题,而是一道「在多个约束条件下如何做取舍」的决策题。面试官想从回答里看到的,是你遇到一个没有标准答案的复杂问题时,能不能先定位、再行动、最后验证。
2. 第一层博弈:资源加载不是「拆包」,而是调度、缓存与优先级
2.1 拆包也有上限:它只解决「传输」问题
先承认拆包的合理性。对单体 bundle 来说,路由懒加载和公共依赖拆分,确实能显著减少首屏下载量。但在工程上,拆包粒度不是越细越好。拆得太粗,首屏会加载用不到的代码;拆得太细,浏览器要多维护大量小请求的调度、解析和执行,在低端机上反而可能变慢。
判断一个拆包方案是否合理,我一般看三个维度:
- 首屏 chunk 的字节数是否接近真实需要的最小值
- 非首屏 chunk 是否真的到对应路由才加载
- vendor 包是否按模块依赖合理切分,而不是把整个 node_modules 抽成一个巨大的 vendor chunk
常见的工程配置包括:把 React、Vue 的运行时单独抽成 framework chunk 并用 contenthash 设置长期缓存;把 echarts、antd 这类重型依赖改成按需引入或动态 import;通过 splitChunks 的 cacheGroups 把变动频繁的业务代码和变动不频繁的依赖代码分开,让浏览器在发版后尽量命中缓存。
一个常见的 webpack 配置示例大致是这样的:
// webpack.config.js 中的拆包思路(示意结构) module.exports = { optimization: { splitChunks: { chunks: 'all', cacheGroups: { framework: { test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, name: 'framework', priority: 40, chunks: 'all', }, vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendor', priority: 20, chunks: 'all', }, }, }, }, };但要注意,拆包只能降低「首屏不必要资源的加载」,并不降低「首屏必须执行的 JS 总量」。如果首屏交互本身就依赖一大段不可拆的业务逻辑,拆包只是把无用代码挪走了,该执行的执行成本一分不少。这是很多人在面试里说不清楚的一点:拆包针对的是传输优化,不是执行优化。
2.2 Preload、Prefetch 与 fetchpriority:把调度权安排明白
浏览器拿到 HTML 后,并不是瞬间知道该下载哪些资源。CSS 和同步 script 会被解析器发现并阻塞渲染,而异步 chunk、图片等资源,往往要等到对应的 DOM 节点被解析到,或者 JS 运行时发起了 import,浏览器才会知道它的存在。这个「资源发现时间差」,有时候比下载耗时更影响首屏。
preload 解决的就是「发现太晚」的问题。它的作用相当于提前告诉浏览器:这个资源现在虽然还没被解析到,但后面一定会用,你先下载着。对首屏来说,值得 preload 的资源通常是首屏 CSS、字体文件,以及最重要的首屏 chunk。
prefetch 则用于「下一步很可能用到的资源」。比如某个用户大概率会点击的按钮对应的异步 chunk,可以在空闲时间静默预下载,等用户真正点击时几乎没有加载感。但这里需要克制:不要把全站路由都 prefetch,否则你只是把首屏省下来的流量在后台又花掉了,还会占用用户的带宽和电池。
fetchpriority 是更细粒度的调度手段。给 LCP 候选图片加上fetchpriority="high",可以避免它与一堆非关键图片在浏览器默认调度策略下排队:
<img src="/hero.avif" fetchpriority="high" alt="首屏主视觉" /> <img src="/banner-bottom.jpg" loading="lazy" alt="页底横幅" />这段示例里,首屏主视觉被标记为高优先级,页底横幅则懒加载。浏览器虽然有启发式调度,但它并不理解页面的语义,无法判断哪张图对用户最重要。标注优先级,就是在替浏览器做业务层面的决策。
2.3 图片、字体与 CDN:首屏体积里真正的大头
实际项目里,首屏资源的大头往往是图片,而不是 JS。一张 2MB 的 hero 图,比 500KB 的脚本更容易拖垮 LCP。所以这一步做的不是拆包,而是素材形态的优化:
- 下一代图片格式(WebP、AVIF)在同视觉质量下通常能显著减小体积
- 响应式图片通过 srcset 和 sizes 让不同屏幕加载不同尺寸,而不是所有设备都加载最大图
- 首屏大图用 fetchpriority="high",非首屏图用 loading="lazy"
- 字体使用
font-display: swap避免字体加载阻塞文字渲染,必要时用 unicode-range 做子集化
CDN 的价值则体现在两个层面:一是让资源离用户更近,缩短 RTT;二是通过合理的缓存策略提高命中率。很多人会忽略 CDN 的缓存配置和发布策略之间的配合。如果文件名不带 hash 而缓存又设得很久,发版后用户可能拿到旧资源;如果文件名带 hash 而缓存时间设置太短,拆包拆出来的长期缓存 chunk 又失去了意义。常见的方案是「不可变内容 + 长缓存,HTML 走短缓存或 no-cache」的配合,确保版本更新可以及时生效。
2.4 HTTP 缓存与发布策略:让长期缓存真正生效
缓存看起来是运维话题,但它直接决定拆包优化的长期收益。如果把缓存分为几层,通常是:
| 层级 | 作用 | 常见配置 |
|---|---|---|
| 浏览器缓存 | 用户二次访问时不发请求 | Cache-Control、ETag |
| CDN 缓存 | 不同用户之间共享缓存 | s-maxage、CDN 节点缓存策略 |
| 服务端缓存 | 减少后端计算和响应时间 | 页面缓存、接口缓存 |
拆包后的 chunk 文件名带 contenthash,本质就是为了让浏览器可以安全地「永久缓存」。发布时只有 HTML 会变化,chunk 内容没变的请求可以直接命中缓存。这也意味着,拆分 vendor 和业务代码的目的不只是首屏,更是让发版后的缓存有效率更高——因为业务代码频繁变动,依赖代码相对稳定。
这个细节在面试里值得主动提。它能把「我会拆包」升级成「我理解拆包背后的缓存复用逻辑」。
3. 第二层博弈:渲染架构决定首屏体验的天花板
3.1 CSR、SSR、SSG、ISR:不是选型题,是取舍题
资源加载做得再极致,如果内容必须等 JS 执行完才能出现,首屏体验依然被渲染架构的天花板压着。所以第二层博弈,是渲染架构的选择。
CSR 的痛点是:用户要等 JS 下载、解析、执行完成,然后才能看到内容。对中低端设备尤其明显。SSR 的价值,不是让页面「看起来多了服务端」,而是让 HTML 本身就携带内容,浏览器在 JS 还没执行时就能先绘制文字和布局。SSG 适合内容相对固定的页面,构建期生成 HTML,访问时直接返回静态文件,首屏通常最快。ISR 则是一种折中,静态页面可以定期或按需重新生成。
面试里常考的,是让你针对一个真实业务场景做选型。这时候不能只背概念,要有判断力:
- 页面内容由登录用户个性化生成,SSG 基本不合适
- 页面每个访问都需要实时数据,SSR 或 CSR 加数据预取更合适
- 营销落地页、文档站、博客这类内容型页面,SSG 性价比最高
- 复杂业务往往是混合架构:首页用 SSG 或 ISR,用户中心用 CSR,活动页用 SSR
这里最容易犯的错误是「因为听说 SSR 对 SEO 好,所以所有页面都上 SSR」。实际上 SSR 带来的成本包括服务端渲染压力、复杂度提升、缓存难度增加,以及 hydration 成本。如果页面本身不需要 SEO,内容也不要求秒开,CSR 配合良好的资源加载是更务实的方案。
3.2 Hydration 成本与 Streaming SSR:内容早到不等于马上可交互
SSR 解决了「内容早出现」,但引入了另一个问题——hydration(水合)。用户看到 HTML 内容后,浏览器还要执行一份 JS,去绑定事件、恢复状态、建立虚拟 DOM 对应关系。如果这份 JS 很大、执行很慢,用户就会进入「看得见、摸不着」的状态:页面已经显示,但点击按钮没有反应。
这也是为什么 Web Vitals 里会有 INP 这样的交互指标。一个 SSR 页面如果 hydration 需要 3 秒,那它在低端机上体验可能比 CSR 还难受——用户已经在阅读内容了,却无法点击。
Streaming SSR 是改善这个问题的重要思路:把 HTML 分块流式输出,让首屏核心区块尽早到达浏览器,非关键区块的 HTML 后到。配合 Suspense 和选择性水合,可以做到「先渲染先水合」,用户不必等整棵组件树全部 ready,就能先与部分区域交互。
这个机制的底层逻辑,是把「一个巨大的串行任务」拆成「多个按需完成的小块」:内容分段到达,交互分段恢复,用户感知到的等待是被稀释的,而不是整体延后。
3.3 Islands 与局部水合:让客户端 JS 总量最小化
如果页面大部分是静态内容,只有评论区、搜索框、点赞按钮这类局部区域需要交互,其实没有必要让整个应用都走全量 hydration。Islands 架构的思路是:页面在服务端渲染成静态 HTML,只有被标记的小块组件在客户端「充水」成为可交互区域。静态部分零 JS,交互部分按需加载。
放到面试语境里,这个考点考察的是:候选人是否理解「客户端 JS 总量」对首屏和交互性能的影响,是否懂得让「需要客户端 JS 的面积」最小化。很多人对 islands 的认知停留在「听说过 Astro、Fresh 用了这个架构」,但说不清楚它和 partial hydration、selective hydration、React Server Components 之间的共性与边界。其实它们的底层逻辑是相通的:减少客户端水合的覆盖面积和成本。
3.4 要不要动架构:先看四类证据
架构改动成本很高,不能凭感觉。我建议在动手前,先检查四类证据:
- 瀑布图显示首屏 HTML 内容为空,主要内容全靠 JS 客户端渲染——说明渲染架构可能有问题。
- TBT 或 INP 长期超标,页面「能看但点不动」——hydration 或执行成本过高。
- 社交分享、搜索引擎抓取到的页面里没有正文内容——CSR 在这种场景下有天然劣势。
- 资源层优化已经做得比较充分,比如压缩、拆包、图片、缓存都到位了,但 LCP 依然不达标——瓶颈大概率在渲染架构层。
只有当证据指向「内容到达太晚」或「水合成本太高」时,才值得引入 SSR 或 Islands 这类架构调整。否则,先用低成本手段把能拿的优化拿到手,再考虑架构。
4. 第三层博弈:可观测性是优化闭环的入口,不是加分项
4.1 本地面板不等于真实用户现场
首屏优化最容易犯的认知错误,是把本地 Chrome DevTools 的 Network 面板当作性能真相。本地是千兆网络、开发者通常用性能不错的电脑、机器上没有大量后台任务——这些条件和真实用户的 Wi-Fi、4G、中低端 Android、WebView 容器比,差距可能是数量级的。
可观测性的意义,在于把「我猜用户慢」升级为「我知道用户慢在哪里、慢在哪些用户身上」。在面试里,能主动提到可观测性的候选人常会被高看一眼,因为这说明你不是在背教程,而是真的管理过一个线上系统的性能。
核心指标可以从 Core Web Vitals 开始:
| 指标 | 含义 | 关注原因 |
|---|---|---|
| LCP | 最大内容绘制 | 用户看到主要内容的时间 |
| INP | 交互到下一次绘制的延迟 | 用户交互的响应速度 |
| CLS | 累积布局偏移 | 页面稳定性 |
| TTFB | 首字节时间 | 服务端响应和网络往返 |
| FCP | 首次内容绘制 | 用户看到任何内容的时刻 |
| TBT | 总阻塞时间 | 长任务累计,影响交互 |
除此之外,还要定义业务自定义指标,比如「商品首图加载完成时间」「首屏数据接口返回时间」「搜索框可输入时间」。只有指标切中你的业务场景,优化才有方向。
4.2 用数据验证拆包:一个灰度对比的示例流程
验证拆包有没有效果,不能只看优化前后页面的均值。更严谨的做法是分组对比:
- 上线前采集至少一周的基线数据,记录 LCP 的 p50、p75、p95。
- 在灰度环境执行优化版本,用同一套指标口径采集。
- 分别分析「有缓存」和「无缓存」用户的差异。
- 对比不同网络档位、不同设备档位下的分布变化。
- 确认提升不是季节性流量或硬件变化带来的。
这里还有一个常见坑:性能指标的平均值很容易被长尾放大。建议优先观察分位数,并单独看低端机和弱网两个细分群体。如果优化只在高端设备上有效,在低端机上反而更慢,那很可能是拆包拆出了太多小文件,导致低端机的解析和执行成本上升。
一个 RUM 上报的示意结构大概是这样:
// 示意:采集一个关键指标并上报 const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { navigator.sendBeacon('/api/perf', JSON.stringify({ name: entry.name, value: entry.startTime, device: navigator.deviceMemory || 'unknown', network: navigator.connection?.effectiveType || 'unknown', page: location.pathname, })); } }); observer.observe({ type: 'largest-contentful-paint', buffered: true });注意这只是一个示例结构,实际项目里还要考虑采样率、上报频率、去重和隐私合规,不能为了监控而额外拖慢页面。
4.3 RUM 埋点要覆盖哪几类数据
做性能优化时,我一般会尽量覆盖四类数据:
- 环境维度:UA、设备内存、网络类型,以及如果是 App 内嵌 WebView,记录具体容器版本
- 资源维度:关键 JS、CSS、图片的 DNS、连接、TLS、TTFB、下载耗时、是否成功
- 渲染维度:FP、FCP、LCP、CLS、INP,以及关键元素的可见时间
- 异常维度:JS 报错、资源加载失败、白屏、接口超时、慢查询
有了这些数据,优化就不是「做一版然后感觉应该会好」,而是一个标准的闭环:采集 → 分层 → 定位瓶颈 → 改动 → 灰度 → 对比 → 再迭代。
5. 面试答题框架:把系统博弈讲成一条完整链路
5.1 一个可复用的五步答题模型
遇到「你怎么做首屏优化」这类问题,可以参考下面的五步框架:
第一步,定义指标。先明确要优化的是 LCP、INP、FCP 还是首屏可交互时间。指标不统一,所有后续讨论都会发散。
第二步,分层诊断。把首屏问题拆成三层:
- 资源层:体积、数量、优先级、缓存、CDN、图片格式
- 渲染层:CSR/SSR/SSG/ISR、hydration、streaming、islands
- 执行层:JS 执行成本、长任务、第三方脚本、数据请求串并行
判断顺序是:先看瀑布图,定位瓶颈发生在网络阶段还是执行阶段,再去具体那一层做深入分析。
第三步,选择手段并设计实验。不要一上来就上 SSR。先从低成本手段开始,比如 preload、图片压缩、字体子集化、缓存策略,观察指标变化;如果不够,再考虑拆包、接口并行、服务端数据注入;最后才评估渲染架构调整。
第四步,通过可观测性验证。确保上线前后能采集分组数据,用分位数对比确认收益,而不是凭感觉说「好像快了」。
第五步,持续迭代。性能优化不能做完一次就结束。它应该进入常规质量标准,每次需求变更后都做性能回归。
5.2 常见追问和应对思路
面试官大概率会沿着你的回答继续追问。几个高频问题提前准备:
- 「你拆包之后,到底哪个指标变好了?」要回到「指标定义 → 基线 → 对比」这条链路,而不是笼统地说「加载快了」。
- 「如果是用户第一次访问,你的优化还成立吗?」缓存相关手段会失效,这时候要讨论首屏 HTML 大小、资源优先级、TTFB。
- 「你的方案对低端机友好吗?」要提到 JS 执行总量、长任务拆分、避免大量 DOM 操作,以及拆包粒度和设备性能之间的关系。
- 「你会做 SSR 吗?为什么不做?」不能只说「SSR 好」或「SSR 配置麻烦」,要说「当前瓶颈在 XX,SSR 能解决/不能解决这个问题,它的增量成本是 XX,所以我选择 / 不选择」。
- 「你怎么知道自己优化成功了?」回到分位数、分组对比、真实用户数据,而不是「感觉快了一些」。
5.3 高质量回答的共同特征:权衡、边界、证据
面试官并不期待你所有手段都亲手做过,而是想听你如何做决策。一个高质量的回答,通常包含三个特征。
权衡:主动说出「我为什么不这么做」。比如「我选了 SSG 而不是 SSR,因为页面内容更新频率低,静态化可以让首屏最快,服务端成本也最低」。
边界:区分「这个方法适合什么场景、不适合什么场景」。比如「prefetch 适合预测用户下一步会访问的页面,但不适合把所有路由都预取」。
证据:每个判断都有依据。「我之所以先拆 vendor 而不是先做 SSR,是因为 RUM 数据显示 LCP 的瓶颈在资源下载阶段,而不在渲染阶段。」
这三者放在一起,才能让面试官相信你具备系统博弈的能力。
6. 从面试到生产:低成本优化路径与排查链路
6.1 先做快赢,再动架构
如果要把这套方法论落到真实项目,我建议的启动顺序是:
- 体检:接入 RUM,同时跑一次 Lighthouse 和 Network 瀑布图分析。
- 快赢:压缩图片、调整字体策略、开启 brotli/gzip、给关键资源加 preload、移除无用第三方脚本。
- 拆包:按路由做按需加载,把框架运行时和业务代码分离,配置 contenthash 长期缓存。
- 缓存与 CDN:确认静态资源缓存策略、CDN 命中率、HTML 与资源的缓存区隔。
- 最后才评估 SSR、Streaming、Islands 这类架构调整。
这个顺序背后的逻辑是:每一步的成本在递增,但每一步都可能解决关键问题。先做低成本的,通常能拿到 60% 到 70% 的收益。
6.2 容易变成「负优化」的四个动作
优化做过头,反而会拖慢页面。常见的四类负优化:
- 拆包拆出了大量小请求,低端机网络调度和解析成本上升,首屏实际变慢。
- 把所有可能访问的路由都 prefetch,用户在后台被消耗了大量流量和电量。
- SSR 没配合 streaming,HTML 要等整棵树渲染完才返回,TTFB 反而变高。
- 加了大量性能监控脚本,监控本身成了首屏负担。埋点要设置采样率,并尽量用 sendBeacon 或异步上报。
6.3 首屏性能问题排查顺序
遇到「首屏慢」的问题,我建议按固定顺序排查,避免东一榔头西一棒子:
- 先厘清现象。是白屏久、图片慢、点击卡,还是布局抖动?不同的现象对应不同的指标。
- 打开 Network 瀑布图,判断是「下载慢」还是「发现晚」。如果资源在页面加载很久之后才被请求,多半缺少 preload 或优先级调度。
- 检查 HTML 内容是否为空。如果 HTML 是空的,基本可以判断是 CSR 渲染链路的问题,资源层优化只能改善一部分。
- 看 JS 执行时长和长任务。Performance 面板里单独看 script 阶段,确认是否阻塞了主线程。
- 检查数据接口。首屏依赖的接口是串行还是并行?能不能在服务端把首屏数据直接注入 HTML?
- 回到真实用户环境。用 RUM 数据导出 p75 甚至 p95 用户的环境、资源加载阶段分布,验证问题和修复效果。
这套顺序的本质是「先定层、再定位」。每一层排除了,再进到下一层,问题不会跑偏。
6.4 记住这个闭环
首屏优化在当下和未来的正确姿势,不是某一条命令、某一个插件,更不是一门心思拆包。它是一个「资源加载 – 渲染架构 – 可观测性」三层交织的系统工程。
拆包解决的是「东西多久能到」;渲染架构解决的是「内容何时出现、何时可交互」;可观测性解决的是「我们怎么知道前两项有没有做好」。三者缺一不可。只看重资源层,你可能连问题都没有定义清楚;只看重渲染层,你会为一个不存在的问题去改架构;只看重观测层,你知道指标不好,却不知道该动哪里。
下次再被问到首屏优化,不妨先不要急着说拆包。先问自己一句:我的用户到底慢在哪里?然后沿着资源加载、渲染架构、可观测性这条链路,把决策路径完整地讲出来。这才是这道题真正想考察的东西。