Chrome 浏览器渲染机制
从输入 URL 到页面显示,Chrome 会把导航、网络和渲染任务分配给不同进程:
- 浏览器进程:负责地址栏、标签页、导航和进程协调;
- 网络进程:负责缓存、DNS、连接、请求、响应和证书校验;
- 渲染进程:负责 HTML 和 CSS 解析、JavaScript 执行,以及样式计算、布局和绘制;
- GPU 进程:负责 GPU 光栅化、显示合成和最终画面输出。
渲染进程内,主线程负责 HTML 解析、JavaScript 执行和大部分渲染工作;合成线程负责光栅任务调度,并可在无需主线程参与的情况下处理部分滚动和动画帧。
导航过程
导航过程主要由浏览器进程和网络进程协作完成,直到目标渲染进程接收文档数据。
导航类型
导航可由地址栏输入、页面操作、重新加载或前进后退触发。确定目标后,浏览器会根据是否继续使用当前 Document,将其分为同文档导航和跨文档导航。
同文档导航保留当前 Document,更新 URL、历史记录或滚动位置等导航状态;跨文档导航会切换 Document,并可能触发 beforeunload 确认。对于跨文档的前进或后退,浏览器会先尝试从往返缓存(bfcache)恢复目标文档,无法恢复时再进入网络加载;其它跨文档导航直接进入网络加载。处理流程如下:
网络请求
跨文档导航进入网络加载后,网络进程负责获取主文档,主要包括:
- HTTP 缓存:缓存仍然有效时直接使用本地副本,需要重新验证时发起条件请求;
- DNS 解析:将域名解析为服务器 IP 地址;
- 建立连接:根据协议建立 TCP、TLS 或 QUIC 连接,并尽量复用已有连接;
- 发送请求:携带请求头、Cookie 和可选的请求体,将请求发送到目标服务器。
后续发现的 CSS、JavaScript、图片等子资源也会复用相同的缓存、解析和连接机制,只是加载时机与优先级不同。
接收响应
网络进程收到响应后,会根据状态码、响应头和内容类型决定后续处理:
- 重定向响应:根据
Location发起新的导航请求; 304 Not Modified:复用本地缓存中的响应体;- HTML 响应:将数据流交给渲染进程,可以边接收边解析;
- 下载文件:交给下载管理器处理。
响应体会在解析前按需解压。HTML 响应准备就绪后,浏览器进程会继续提交导航。
提交导航
Chrome 通常会同时运行多个渲染进程,并通过站点隔离策略将不同站点的数据置于不同的进程内,降低单个渲染进程被攻破后的影响范围。提交导航前,浏览器进程会确定由哪个渲染进程承载目标文档。同站点导航通常复用当前进程,跨站点导航通常切换到承载目标站点的进程;没有可复用的进程时,Chrome 会创建新进程。
目标进程准备好后,浏览器进程会提交导航,将响应数据交给该进程,同时更新地址栏、历史记录和标签页状态。导航提交完成后,目标渲染进程开始解析 HTML,后续发现的子资源仍由网络进程加载。
HTML 解析与资源加载
导航提交后,渲染进程会增量解析 HTML,并在解析过程中发现和加载页面依赖的资源。
HTML 解析
响应数据进入渲染进程后,主线程会增量解析 HTML,逐步构建 DOM,无需等待整份 HTML 下载完成。在此过程中,解析器也会发现外部资源,不同资源对解析和渲染的影响不同:
- CSS:浏览器下载外部样式表并解析为 CSSOM。默认情况下,解析器发现的外部样式表会阻塞后续渲染,但不会直接阻塞 HTML 解析。同步脚本可能读写 CSSOM,执行前必须等待前面的阻塞样式表下载并解析完成,因此 CSS 会间接阻塞 HTML 解析;
- JavaScript:同步脚本会阻塞 HTML 解析,直到执行完成。
async、defer和模块脚本(type="module")都会并行下载;async脚本下载完成后执行,defer和模块脚本默认在 HTML 解析完成后执行; - 图片、字体和视频:通常不阻塞 HTML 解析,但下载、解码和尺寸变化可能影响后续布局与绘制。
子资源加载
主解析器按顺序解析 HTML,遇到同步脚本时会暂停。为了避免后续资源等到解析恢复后才被发现,预加载扫描器会预解析尚未处理的 HTML,提取其中直接声明的样式表、脚本和图片等资源 URL,并将请求交给网络进程,提前开始下载。
预加载扫描器只负责发现 HTML 中直接声明的资源,不构建 DOM,也不执行脚本。字体等由 CSS 引用的资源通常需要等 CSSOM 解析到相应规则后才能发现,除非通过 <link rel="preload"> 等方式提前声明。资源仍由网络进程加载,并根据资源类型、发现位置、渲染阻塞关系、视口位置和 fetchpriority 等信息动态调整优先级。
资源加载优化
优化的重点是缩短关键渲染路径,并避免非关键资源争用带宽:
- HTML:优先输出页面结构和关键资源声明,避免通过脚本延迟创建首屏内容;
- JavaScript:依赖 DOM 或执行顺序的脚本使用
defer,独立脚本使用async,其它模块按路由或功能动态导入; - CSS:仅在能够度量收益时内联少量首屏关键样式;
- 资源发现与优先级:只对浏览器无法及时发现的关键资源使用
<link rel="preload">;首屏核心图片可以设置fetchpriority="high",但不要同时提高过多资源的优先级; - 图片和字体:根据展示尺寸选择合适的图片分辨率和编码格式,视口外图片设置
loading="lazy",字体则根据字形切换和可见性要求选择font-display策略。
最后通过 Network 面板检查资源的发现时机、加载优先级和实际调度结果。
JavaScript 执行
页面 JavaScript 通常在渲染进程主线程执行,与事件处理、样式计算、布局和绘制共享主线程。脚本持续占用主线程时,这些工作也会被推迟。
脚本修改 DOM 或影响布局的样式后,原有布局结果可能失效。此时读取 offsetWidth 或 getBoundingClientRect() 等信息需要返回最新结果,可能迫使浏览器同步执行样式计算和布局,这称为强制同步布局。反复交错读写会进一步形成布局抖动。
长任务会推迟输入处理和新帧生成。非关键工作应延后执行,耗时工作可拆分为多个短任务;涉及布局信息时,先批量读取,再批量修改 DOM 和样式。
渲染流水线
DOM、样式或脚本改变页面状态后,Chrome 会根据变化类型执行必要的渲染阶段。下面依次介绍样式计算、布局、绘制、分层、光栅化和合成;实际更新时,未受影响的阶段会被跳过。
以下内容基于 Chrome 当前的 RenderingNG 架构 展开。
样式计算
CSS 解析后生成 CSSOM。浏览器结合 DOM、CSSOM、内联样式和浏览器默认样式,为每个元素生成计算样式(computed style):
- 根据元素及其上下文匹配选择器,并结合伪类状态、媒体查询等条件筛选当前生效的声明;
- 按照 CSS 层叠规则处理冲突,确定每个属性采用的声明;
- 对没有有效声明的属性应用继承值或初始值;
- 按属性定义解析关键字和相对单位,将属性值标准化为计算值。部分依赖布局的信息会留到布局阶段确定。
样式计算的开销主要取决于需要重新计算的元素数量和选择器匹配成本。控制 DOM 规模、对长列表使用虚拟列表,并缩小样式变化的影响范围,可减少重新计算的元素;保持选择器简洁则能降低匹配成本。
布局
样式计算完成后,浏览器会为需要参与布局的内容构建布局树,并计算各布局对象的尺寸和位置。布局树与 DOM 并非一一对应:display: none 元素不会生成布局对象,visibility: hidden 元素仍参与布局,需要显示的伪元素和文本内容也会生成布局对象,但它们没有对应的 DOM 元素。
DOM 或样式变化影响几何信息时,浏览器需要重新计算受影响的布局,这通常称为重排(reflow)。重排范围由布局依赖关系决定,不一定覆盖整个页面;涉及的布局对象越多,开销越大。常见的控制方式包括:
- 位移和淡入淡出动画优先使用
transform和opacity,避免通过几何属性触发重排; - 对布局相对独立的区域使用合适的
contain值,限制样式、布局或绘制的影响范围; - 为图片、广告位和异步内容预留尺寸,减少布局偏移。
绘制
布局完成后,浏览器先在预绘制阶段根据布局结果更新变换、裁剪、视觉效果和滚动等属性树,并标记失效区域;随后在绘制阶段结合布局结果、计算样式和属性树,将背景、边框、文字、图片和阴影等视觉内容记录为有序的绘制列表(display list)。这一阶段只生成绘制记录,不生成屏幕像素。
颜色、背景或阴影等视觉属性变化时,原有绘制记录可能失效,浏览器需要重新生成受影响区域的绘制指令,这通常称为重绘。重绘不需要重新布局,但更新后的内容仍需光栅化并参与合成。
重绘开销主要取决于失效区域的大小和视觉效果的复杂度。应减少复杂阴影、滤镜、渐变和半透明效果的覆盖范围与变化次数。对于频繁变化的复杂阴影,可以将其绘制在伪元素上,交互时只改变 opacity,并通过 DevTools 确认是否避免了重复绘制。
分层
绘制完成后,主线程会将属性树和绘制列表提交给合成线程。合成线程在分层阶段结合两者,将绘制记录组织为合成图层列表,使不同图层能够独立光栅化或执行动画。
CSS 层叠上下文决定内容的层叠与绘制顺序,但不直接对应合成图层。浏览器根据滚动、动画和其它合成需求,决定绘制内容使用独立合成图层,还是与其它内容共享合成图层。因此,合成图层与 DOM 元素或层叠上下文都不是一一对应关系。
独立合成可以让部分动画和滚动由合成线程处理,避免重复布局和绘制;图层过多则会占用更多内存,增加光栅化和合成成本。
will-change 用于提示浏览器元素即将发生变化,但不保证创建独立合成图层。确认存在性能问题后,只对少量即将变化的元素临时声明,例如 will-change: transform,并在变化结束后移除。
光栅化
光栅化负责将各合成图层对应的绘制记录转换为像素资源。完成分层后,合成线程会把合成图层划分为图块(tile),根据每个图块包含的绘制内容生成光栅任务并交给栅格化线程池,优先光栅化视口内以及即将进入视口的图块。按图块处理可以避免一次性光栅化整个图层。
执行光栅任务时,栅格化线程会处理绘制指令;如果图块包含图片,还需要取得相应的解码结果。Chrome 通常使用 GPU 加速光栅化:栅格化线程通过命令缓冲区向 GPU 提交绘制指令,由 GPU 生成纹理图块。无法使用 GPU 光栅化时,则由 CPU 生成软件位图;如果后续使用 GPU 合成,还需将位图上传到 GPU 内存。
光栅化开销主要受图块数量、绘制内容复杂度和图片解码成本影响。大图片或大面积图层会占用更多时间与内存;快速滚动时,如果新图块未能及时完成光栅化,可能出现内容缺失或掉帧。
合成与显示
光栅化完成后,渲染进程的合成线程会根据各图块的位置、变换和透明度等信息,生成绘制四边形(draw quad),组成合成帧并提交给 GPU 进程中的显示合成器。显示合成器汇总页面内容与浏览器界面的合成帧,再交由 GPU 合成最终画面并输出到屏幕。
当动画只改变 transform 或 opacity,且元素已提升为合成图层时,后续帧通常可以直接在合成阶段处理,无须经过主线程的样式计算、布局和绘制。省去这些工作后,动画通常会更加流畅,但元素是否提升为合成图层仍由浏览器决定。
合成图层过多会增加 GPU 内存占用和合成开销。可以使用 DevTools 的 Layers 和 Performance 面板查看分层原因、图层数量和合成耗时,确认动画是否实际走合成路径,避免不必要的图层提升。