从输入 URL 到像素的旅程
在用户眼里,输入网址到看到页面是一瞬间的事;在浏览器内部,这趟旅程要经过好几个站:请求 HTML、解析成 DOM 树、解析 CSS 成样式树、两棵树合并成渲染树、计算每个元素的位置(布局)、画成像素(绘制)、最后合成到屏幕。理解这条流水线,是所有首屏优化的前提——不知道卡在哪一站,优化就是瞎猜。
渲染优化的本质,是让关键内容尽可能早地走完这条流水线,而不是让 JS 和 CSS 把路堵住。
- 渲染管线解析 → 布局 → 绘制 → 合成,四步各有成本。
- 重排几何变化触发重新布局,成本最高。
- 重绘外观变化只重画,不重排,成本较低。
- 关键 CSS首屏样式内联,避免等外部 CSS 才渲染。
关键渲染路径
浏览器拿到 HTML 后不会立刻渲染:遇到 CSS 要先等它加载解析,因为没有样式就不知道元素长什么样;遇到没有 defer 的 JS 也要停下来,因为 JS 可能会改 DOM 和样式。这意味着首屏快慢,很大程度上取决于 HTML、CSS、JS 三个资源的加载与解析顺序。
关键渲染路径优化的目标,就是缩短"从开始加载到首次渲染"的距离:让 CSS 尽快可用、让非关键 JS 不要阻塞、让首屏内容在 HTML 里靠前。
重排与重绘:什么时候变慢
页面加载完成后,交互时的卡顿多来自重排(Reflow)与重绘(Repaint)。修改元素的宽度、高度、位置、字体大小,会触发浏览器重新计算布局(重排),连带影响周围元素;只改颜色、背景,不影响布局的,只触发重绘。重排比重绘贵得多,因为它要重新走一遍布局计算。
// 触发重排:改宽度,整个布局都要重算
el.style.width = '300px';
// 只触发重绘:改颜色,不动布局
el.style.color = 'blue';
// 性能写法:先离线改完,再一次性放回去
el.style.display = 'none';
update(el); // 期间的多次改动只引发一次重排
el.style.display = 'block';
频繁读取 offsetTop 再立即改样式,会强制浏览器同步布局(Layout Thrashing),是动画卡顿的常见原因。
JS 阻塞了渲染的什么
JS 之所以阻塞渲染,是因为它可能改动 DOM 结构和样式。浏览器解析到一个不带 defer/async 的脚本时,会停下来先下载执行它,再继续解析后面的 HTML。所以把巨大的 JS 放在 head 里,首屏就要先等它执行完。
解决思路:非关键脚本加 defer(等 HTML 解析完再执行,不阻塞渲染)、关键路径之外的代码异步加载、首屏用服务端渲染或静态 HTML 直出,让用户先看到内容,JS 再在后台接管交互。
首屏优化:关键 CSS 与骨架屏
两个最有效的首屏手段。关键 CSS 内联:把首屏可见区域用到的样式直接写在 HTML 的 style 标签里,浏览器不用等外部 CSS 文件就能先把首屏画出来,其余样式异步加载。骨架屏:在真实内容到达前,先显示和布局一致的灰色占位块,让用户感知"页面正在加载"而不是一片白。
<!-- 关键 CSS 直接内联在 head,不等外部文件 -->
<style>
.hero{height:40vh;background:linear-gradient(135deg,#1e5fa8,#3aa0e8)}
.nav{display:flex;align-items:center;gap:16px}
</style>
<link rel="stylesheet" href="/rest.css"> <!-- 其余样式异步加载 -->
动手测量
优化后要用浏览器的 Performance 面板录制一次加载,看火焰图里哪一步最久:是 HTML 下载、CSS 阻塞、JS 执行还是布局绘制。把最长的那一段找出来针对性优化,而不是凭感觉猜。配合 Core Web Vitals 的 LCP(最大内容绘制)作为首屏快慢的量化指标。
小结
把渲染原理用到首屏优化上,记住主线:
- 先懂渲染流水线,再谈优化,别凭感觉。
- 避免重排:批量改样式、离线节点操作。
- 非关键 JS 加 defer,别阻塞首屏解析。
- 关键 CSS 内联 + 骨架屏,让首屏先可见。
- 用 Performance 面板与 LCP 量化验证效果。