浏览器渲染机制踩坑记录

一开始以为是网络问题,服务器带宽不够,或者 CDN 没配置好。

真的慢,从输入 URL 到看到完整页面,平均要 8 秒。

问题是怎么来的

项目上线之后,用户反馈首页慢。真的慢,从输入 URL 到看到完整页面,平均要 8 秒。

一开始以为是网络问题,服务器带宽不够,或者 CDN 没配置好。但后来在本地测,同样的机器,打开别人的网站很快,我们的还是很慢。

才意识到:问题不在网络,在代码。

浏览器渲染的基本流程

先搞清楚浏览器是怎么把 HTML 变成页面的。

graph TB A[用户输入URL] --> B[网络请求] B --> C[HTML解析] C --> D[DOM树构建] C --> E[CSS解析] E --> F[CSSOM树构建] D --> G[渲染树构建] F --> G G --> H[布局 Layout] H --> I[绘制 Paint] I --> J[合成 Composite] J --> K[显示页面] style C fill:#FFD700 style H fill:#90EE90 style I fill:#87CEEB style J fill:#FFB6C1

关键点:

  • HTML 解析:把 HTML 字符流变成 DOM 树
  • CSS 解析:把 CSS 变成 CSSOM 树
  • 渲染树:DOM + CSSOM 的结合,只包含可见元素
  • Layout:计算每个元素的位置和大小
  • Paint:把元素画成像素
  • Composite:把多个图层合成最终画面

踩过的坑

坑一:脚本阻塞渲染

第一次打开 Performance,发现主线程有一大段时间是白的,啥都没干。

查了半天发现是 <script> 标签的问题。代码里把所有 JS 都放在了 <head> 里:

<head>
  <script src="jquery.min.js"></script>
  <script src="main.js"></script>
  <script src="analytics.js"></script>
</head>

浏览器解析 HTML 遇到 <script> 就停住,等脚本下载完、执行完,再继续解析。JS 没执行完,CSSOM 也没法构建,整个渲染就卡住了。

解决

<head>
  <!-- CSS 放 head,阻塞但必须 -->
  <link rel="stylesheet" href="main.css">
</head>
<body>
  <!-- HTML 内容 -->
  <div>页面内容</div>

  <!-- JS 放 body 底部 -->
  <script src="jquery.min.js"></script>
  <script src="main.js"></script>
</body>

或者用 defer

<script src="main.js" defer></script>

defer 让脚本在 HTML 解析完之后、DOMContentLoaded 之前执行,不阻塞渲染。

坑二:强制同步布局(Layout Thrashing)

优化完脚本加载,以为问题解决了。结果再看 Performance,还是有很多红色的时间块。

仔细一看,发现是代码里有个循环一直在读布局属性:

// 错误写法
for (let i = 0; i < 100; i++) {
  divs[i].style.width = divs[i].offsetWidth + 10 + 'px';
}

这个写法的问题在于:每次写 style.width 之后,浏览器需要重新计算布局;下一次循环读 offsetWidth 时,浏览器发现布局变了,又得重新计算。

这就是强制同步布局:你强制浏览器在每次循环里都重新布局,性能直接炸了。

正确写法

// 先读
const widths = divs.map(div => div.offsetWidth);

// 后写
for (let i = 0; i < 100; i++) {
  divs[i].style.width = widths[i] + 10 + 'px';
}

或者用 requestAnimationFrame

function animate() {
  // 读
  const height = element.offsetHeight;

  requestAnimationFrame(() => {
    // 写
    element.style.height = height + 10 + 'px';
  });
}

坑三:动画性能差

页面有个轮播图,用户反馈滑动的时候卡顿。

打开 Performance 看,发现每次滑动画都会触发 Layout 和 Paint:

/* 错误写法 */
.carousel {
  position: absolute;
  left: 0;
  transition: left 0.3s ease;
}

修改 left 会触发完整的 Layout → Paint → Composite 流程,每秒 60 帧的话,每 16 毫秒要走一遍,主线程根本扛不住。

正确写法

/* 正确写法 */
.carousel {
  transform: translateX(0);
  transition: transform 0.3s ease;
  will-change: transform;
}

transformopacity 只会触发 Composite,在 GPU 里完成,不占主线程。

will-change 提前告诉浏览器这个元素会变,让浏览器提前优化。

graph TB subgraph 低效动画 A[修改 width/height/left/top] --> B[触发 Layout] B --> C[触发 Paint] C --> D[触发 Composite] end subgraph 高效动画 E[修改 transform/opacity] --> F[直接触发 Composite] end style B fill:#FFB6C1,stroke:#FF0000,stroke-width:2px style F fill:#90EE90,stroke:#006400,stroke-width:2px

坑四:资源加载顺序不对

首页加载的时候,图片和 JS 一起发请求,CSS 还没加载完就开始渲染,页面闪白屏。

浏览器默认的加载顺序其实是合理的:HTML 解析时遇到 CSS 就加载,遇到 JS(不带 defer)就停住。但问题是我们的资源太多,关键资源和非关键资源混在一起了。

解决

<head>
  <!-- 关键 CSS 立即加载 -->
  <link rel="stylesheet" href="critical.css">

  <!-- 预加载关键图片 -->
  <link rel="preload" href="hero.jpg" as="image">

  <!-- 非关键 CSS 延迟加载 -->
  <link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
</head>
<body>
  <!-- 首屏内容 -->
  <div class="hero">
    <img src="hero.jpg">
  </div>

  <!-- 非关键脚本延迟加载 -->
  <script src="analytics.js" defer></script>
</body>

关键渲染路径(Critical Rendering Path)里的资源优先加载,非关键资源往后放。

优化后的效果

折腾完这些,再测性能:

指标优化前优化后改善
首屏时间8s1.2s85%
FCP4.5s0.8s82%
LCP7.8s1.5s81%
Layout 耗时1200ms50ms96%

用户不再投诉慢了,运营也说数据好多了。

工具推荐

推荐几个实用的分析工具:

Chrome DevTools Performance

  • 记录完整的渲染流程
  • 看每个阶段的具体耗时
  • 定位 Layout Thrashing 和长任务

Chrome DevTools Coverage

  • 看哪些 CSS 和 JS 是没用的
  • 减小资源体积

Chrome DevTools Rendering

  • 勾选 “Paint flashing” 看哪些元素在重绘
  • 勾选 “Layout shifts” 看布局偏移

Lighthouse

  • 自动审计性能
  • 给优化建议

写在最后

浏览器渲染机制这东西,理论讲再多不如实际踩一次坑。

但也不是所有问题都要优化到极致。小流量、低并发、用户不敏感的场景,正常写就行。等流量上来了、用户开始投诉了,再针对性优化。

优化的核心是理解瓶颈在哪里,不是盲目加缓存、用 WebP、上 CDN。先把渲染流程搞懂,问题自然就好定位了。


这次优化花了半个月,中间走了不少弯路。但回头看,搞懂浏览器渲染这个事,对理解前端性能确实有很大帮助。

版权声明: 本文首发于 指尖魔法屋-浏览器渲染机制踩坑记录https://blog.thinkmoon.cn/post/13-browser-rendering-performance/) 转载或引用必须申明原指尖魔法屋来源及源地址!