第四章 常见性能瓶颈分析:网络、渲染、脚本

做动态交付启动速度优化,第一步不是动手改代码,而是先搞清楚——到底慢在哪儿。

我见过不少团队,一上来就怼着渲染层猛优化,结果发现瓶颈其实在网络请求上。白忙活两周。所以这一章,咱们把最常见的三大瓶颈区域——网络、渲染、脚本——一个一个拆开看。

4.1 网络瓶颈:首屏的“第一公里”

用户打开页面,第一个等待就是网络。说白了,资源没到,什么都干不了。

4.1.1 关键指标

指标说明常见问题
DNS 解析域名转 IP 的时间DNS 缓存未命中、多级 CNAME
TCP 连接三次握手耗时高延迟网络、未用 HTTP/2
TLS 握手HTTPS 加密协商证书链过长、未开启 0-RTT
首字节时间 (TTFB)服务端响应第一个字节动态接口慢、未用 CDN 缓存
资源下载CSS/JS/图片传输未压缩、未分片、请求数过多

我个人习惯,先看 TTFB。如果 TTFB > 800ms,服务端和网络链路大概率有问题。我曾经在一个项目里,TTFB 飙到 2.3s,查了半天发现是后端接口里有个没必要的数据库全表扫描……砍掉后降到 300ms。

避坑指南:别只盯着资源大小。有时候一个 10KB 的 CSS 文件因为放在香港服务器,内地用户加载要 1.5s。地理分布比文件大小更关键。

4.1.2 网络优化三板斧

  1. CDN 加速:静态资源全量上 CDN,边缘节点就近回源。
  2. 预连接与预加载:用 <link rel="dns-prefetch"> 提前解析域名,<link rel="preconnect"> 提前建立连接。
  3. 资源合并与压缩:JavaScript 和 CSS 打包成少量文件,用 Brotli 或 Gzip 压缩。我一般要求打包后单文件不超过 100KB。

试一下这个:在 <head> 里加上 <link rel="preload" href="#" aria-disabled="true" data-keke-missing-local-link="1" onclick="return false" as="style">。首屏关键资源优先加载,效果很明显。

4.2 渲染瓶颈:浏览器怎么“画”出页面

资源到了浏览器,渲染引擎开始工作。这块最容易被忽视——你想想看,用户下载完页面,但白屏还持续了 1 秒,那基本就是渲染的锅。

4.2.1 关键渲染路径

浏览器渲染流程大致是:HTML 解析 → CSSOM 构建 → 布局 → 绘制 → 合成。每步都可能卡住。

  • HTML 解析阻塞:<script> 标签不加 async 或 defer,会阻塞 DOM 解析。
  • CSSOM 构建阻塞:CSS 文件未提前加载,渲染树要等全部 CSS 下载完才生成。
  • 布局抖动:JavaScript 在渲染过程中反复读写 DOM 位置(比如 offsetTop),强制触发回流。
  • 大图层问题:整个页面在一个图层里重绘,GPU 压力大。

我曾在一个 H5 页面里,看到开发者用 getComputedStyle() 写在一个循环里,每次循环都触发布局。那个页面在低端机上白屏了 4 秒……改成分批处理之后,降到 800ms。

4.2.2 渲染优化要点

优化手段作用建议
关键 CSS 内联减少首屏 CSS 请求首屏样式小于 14KB,直接写在 <style> 里
异步加载非关键 CSS不阻塞渲染用 media="print" onload="this.media='all'" 技巧
减少 DOM 深度降低布局计算量嵌套不超过 5 层,使用 Flexbox 代替复杂 float
避免强制同步布局防止布局抖动读写 DOM 属性分批,或用 requestAnimationFrame 分组

嗯,这里要注意:字体加载也是渲染瓶颈大户。换成可变字体或者 font-display: swap,能避免 FOIT(Flash of Invisible Text)。

4.3 脚本瓶颈:JavaScript 的执行代价

说实话,现代前端应用体积越来越大。一个单页应用打包后 5MB 的 JS 并不稀奇。但浏览器解析和执行 JS 是需要时间的——尤其是在低端设备上。

4.3.1 脚本的三个“烧钱”阶段

  • 下载:JS 文件通过网络传输。
  • 解析与编译:浏览器把代码转成 AST,再编译成字节码。这一步在 Chrome 里叫「Parse & Compile」。
  • 执行:运行 JavaScript 逻辑,可能引发 DOM 操作和布局变化。

你可能会说:那我把 JS 都异步加载不就行了?但别忘了——异步加载的 <script async> 虽然不阻塞 HTML 解析,但它一旦下载完就会暂停解析器立即执行。大量异步脚本同时触发,照样会造成主线程拥堵。

我的经验:有一次优化一个报表页面,Chrome DevTools Performance 面板里看到一个叫「Function Call」的红色长条占了 1.6s。进去一看,是一个第三方埋点 SDK 在初始化时遍历了 500 个 DOM 节点。我们把它改成懒加载,只在用户点击「确认」后再执行,首屏启动时间直接降了 35%。

4.3.2 脚本优化清单

  1. 代码分割:按路由或组件分包,首屏只加载必要的 chunk。
  2. 移除无用代码:Tree Shaking + 依赖分析。我用 webpack-bundle-analyzer 看过很多项目,平均有 20% 的代码从未被调用。
  3. 第三方脚本后置:广告、统计、客服聊天等非关键脚本,全部加上 async 或 defer,或者放到 requestIdleCallback 里执行。
  4. 避免长任务:主线程上超过 50ms 的任务就是长任务,会阻塞用户交互。用 setTimeout 或 scheduler.postTask 拆分。

一个小技巧:用 performance.measureUserAgentSpentTime() 监测脚本执行耗时,可以精准定位到哪些函数是「性能杀手」。

4.4 三大瓶颈的联动关系

网络、渲染、脚本不是孤立的。它们互相影响:

  • 网络慢 → 资源到达晚 → 渲染被延后 → 脚本执行也晚。
  • 渲染阻塞 → 主线程卡住 → 脚本无法及时处理 → 用户体验变差。
  • 脚本执行过程中触发布局抖动 → 又反过来拖慢渲染。

所以优化的时候要有全局视角。我个人的做法是:先拉一条完整的水位线(用 Lighthouse 或 WebPageTest 跑一次),然后按「影响面从大到小」的顺序逐一排查。

常见性能瓶颈分析 · 知识结构图 网络瓶颈 DNS / TCP / TLS / TTFB 渲染瓶颈 关键渲染路径 / 布局抖动 脚本瓶颈 解析 / 编译 / 长任务 CDN + 预连接 dns-prefetch 资源压缩 Brotli / Gzip 关键CSS内联 首屏 <14KB 避免回流 requestAnimationFrame 代码分割 按路由分包 Tree Shaking 移除无用代码 第三方后置 async/defer 拆分长任务 setTimeout拆分 联动关系:网络慢 → 资源晚到 → 渲染延迟 → 脚本执行延后 → 交互卡顿 优化建议:先测水位线(Lighthouse),再按影响面从大到小逐一排查

整体来看,网络、渲染、脚本三个维度各有侧重,但在真实项目里往往交织在一起。我建议你每次优化前先做一次完整的 Performance 录制,标记出最大的红条或长任务,然后对照本章表格对症下药。