浏览器渲染机制踩坑记录
一开始以为是网络问题,服务器带宽不够,或者 CDN 没配置好。
真的慢,从输入 URL 到看到完整页面,平均要 8 秒。
问题是怎么来的
项目上线之后,用户反馈首页慢。真的慢,从输入 URL 到看到完整页面,平均要 8 秒。
一开始以为是网络问题,服务器带宽不够,或者 CDN 没配置好。但后来在本地测,同样的机器,打开别人的网站很快,我们的还是很慢。
才意识到:问题不在网络,在代码。
浏览器渲染的基本流程
先搞清楚浏览器是怎么把 HTML 变成页面的。
关键点:
- 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;
}
transform 和 opacity 只会触发 Composite,在 GPU 里完成,不占主线程。
will-change 提前告诉浏览器这个元素会变,让浏览器提前优化。
坑四:资源加载顺序不对
首页加载的时候,图片和 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)里的资源优先加载,非关键资源往后放。
优化后的效果
折腾完这些,再测性能:
| 指标 | 优化前 | 优化后 | 改善 |
|---|---|---|---|
| 首屏时间 | 8s | 1.2s | 85% |
| FCP | 4.5s | 0.8s | 82% |
| LCP | 7.8s | 1.5s | 81% |
| Layout 耗时 | 1200ms | 50ms | 96% |
用户不再投诉慢了,运营也说数据好多了。
工具推荐
推荐几个实用的分析工具:
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/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。