打开一个网页看似只是“请求并显示”,实际上浏览器要完成导航、网络、解析、样式计算、布局、绘制、合成以及脚本执行等一连串工作。理解这条链路的价值,不是记住术语,而是知道页面慢时应该观察哪一段证据。
问题背景:页面为什么不是一次画出来的
浏览器拿到的是连续到达的字节流,而不是一张完整图片。HTML 可能先到,样式表稍后到,脚本又可能继续修改文档。浏览器必须一边接收、一边解析,并在“尽快显示”和“避免反复重算”之间取舍。
常见的三个时间点也并不相同:看到首屏内容、主要内容完成、页面可以流畅响应输入。一个页面可以很早出现骨架,却因为主线程仍在执行长任务而无法点击;也可能已经可操作,但大图稍后加载导致布局跳动。
导航与网络请求
导航开始后,浏览器会解析 URL、检查缓存与 Service Worker,再进行 DNS 查询、建立连接并发送 HTTP 请求。HTTPS 还需要完成 TLS 协商。连接能否复用、服务端何时返回首字节、关键资源是否需要额外往返,都会影响后续渲染何时启动。
01地址解析 → 缓存检查 → DNS → TCP/QUIC → TLS → HTTP 请求 → 响应字节流
HTML 中发现的 CSS、JavaScript、字体和图片会形成新的请求。浏览器有预加载扫描器,但它无法消除错误的资源优先级。首屏大图、关键样式和普通分析脚本不应拥有相同优先级。
图中 HTML 是后续资源发现的入口。CSS 决定样式,JavaScript 可能读取或修改 DOM,而图片与字体会影响最终布局和绘制。
解析与渲染树
HTML 解析器把标记转换成 DOM。CSS 解析器把样式规则转换成 CSSOM。渲染树由可见 DOM 节点和它们的计算样式组成;display: none 的元素不会进入渲染树,但仍存在于 DOM。
外部样式通常会阻塞首次渲染,因为浏览器不能在不知道样式的情况下可靠绘制页面。普通同步脚本则可能暂停 HTML 解析:脚本可能调用 document.write,也可能查询前面节点的样式。defer 脚本在文档解析完成后按顺序执行,通常更适合应用入口;async 脚本下载完成即执行,顺序不确定,适合彼此独立的脚本。
01<link rel="preload" href="/hero.webp" as="image" fetchpriority="high" />02<script src="/app.js" defer></script>
预加载只应用于确实关键且很快会使用的资源。滥用会挤占带宽,反而拖慢 CSS 或字体。
布局、绘制与合成
样式计算确定每个节点的最终属性;布局根据盒模型、字体和视口计算几何位置;绘制把背景、文字、边框等转成绘制指令;合成线程再把不同图层组合到屏幕。
修改不同属性的成本不同。改变 width 往往需要重新布局并绘制;改变 color 通常只需绘制;对独立合成层做 transform 或 opacity 动画,常常可以避免主线程布局。但“提升为图层”会占用显存,不能把所有元素都加上 will-change。
01.moving-card {02 transform: translate3d(var(--x), 0, 0);03 transition: transform 180ms ease;04}0506@media (prefers-reduced-motion: reduce) {07 .moving-card { transition: none; }08}
可交互不等于视觉完成
DOMContentLoaded 表示 HTML 已解析且延迟脚本执行完成;load 还会等待图片等依赖。它们都不能直接代表真实用户体验。现代性能分析更关注:
- LCP:主要内容何时完成绘制;
- INP:用户交互到下一帧呈现的延迟;
- CLS:生命周期内意外布局偏移的程度。
如果 JavaScript 在主线程连续运行 300 毫秒,即使按钮已经绘制出来,输入也只能排队。解决方法不是添加加载动画,而是减少工作、拆分长任务、延后非关键逻辑,并验证拆分后是否真的改善 INP。
实践:减少关键渲染路径
下面是一组可执行的基础策略:
- 服务端尽早返回有意义 HTML,而不是只返回空容器。
- 将首屏必要样式与非关键样式区分,删除未使用 CSS。
- 应用脚本使用
defer或模块脚本,第三方脚本延后。 - 为图片声明尺寸,避免加载后推开正文。
- 对首屏图片使用合适格式、尺寸和优先级。
- 用 Performance 面板和真实用户数据验证,而不是凭感觉优化。
01const observer = new PerformanceObserver((list) => {02 for (const entry of list.getEntries()) {03 console.log(entry.entryType, entry.name, Math.round(entry.startTime));04 }05});0607observer.observe({ type: "largest-contentful-paint", buffered: true });
这段代码适合学习和本地诊断。正式采集时要控制采样、清理可能包含业务信息的资源名称,并把指标与页面版本、设备环境关联。
常见误区与总结
“DOM 少就一定快”忽略了样式复杂度、资源体积和脚本成本;“使用 GPU 就一定快”忽略了图层内存与合成成本;“开发机很流畅”也不能代表低端移动设备。
可靠的方法是先把问题放到网络、解析、布局、绘制或主线程任务中的某一段,再用瀑布图、Performance 记录和 Web Vitals 验证。延伸阅读可以继续进入 JavaScript 事件循环、真实用户监控和图片加载策略。