浏览器实践

浏览器实践

1. 浏览器的渲染过程

从html、css、javascript到页面是如何实现的?

构建DOM树

一个页面的渲染是从html开始的,html描述了这个页面需要的部分资源(css、图片、javascript),也描述了页面的内容。在这之后会有css的样式计算,布局计算等。这样一个处理过程可以称为渲染流水线。

graph LR
  构建DOM树 --> 样式计算 --> 布局阶段 --> 分层 --> 绘制 --> 分块 --> 光栅化 --> 合成
graph LR
  构建DOM树 --> 样式计算 --> 布局阶段 --> 分层 --> 绘制 --> 分块 --> 光栅化 --> 合成
graph LR
  构建DOM树 --> 样式计算 --> 布局阶段 --> 分层 --> 绘制 --> 分块 --> 光栅化 --> 合成

样式计算

在解析完html后,浏览器构建完DOM树,接着会着手样式的计算。

样式的来源有三个地方:

  1. 引入的外部css文件;
  2. style标签内描述的样式;
  3. 元素上内嵌的style属性;
  • 浏览器会将所有的css样式转换为其可理解的结构 —— styleSheets。可以通过document.styleSheets 在浏览器中查看。
  • 对于结构化之后的数据,浏览器还要对其属性进行标准化,例如2em 转换为36pxbold 转换为700 等。
  • 最后根据css的继承规则和层叠规则计算出DOM树中每个节点的样式属性。

布局阶段

创建布局树(Layout Tree)

DOM树在整合了样式后,还缺少布局位置等信息,以及部分DOM节点并不需要实际绘制,例如head 标签,以及使用了display:none 属性的元素。浏览器会遍历DOM树中所有节点,将可见的节点放到布局树中,不可见的忽略。

布局计算

浏览器计算各个节点的坐标位置

分层(生成Layer Tree)

浏览器的页面实际分成了很多层,每个节点直接或间接地属于一个图层 拥有层叠上下文属性的元素会被提升为单独的一层,需要剪裁(clip)的地方也会被创建为图层。

图层绘制(tiles)

栅格化操作(raster)

合成和显示(draw quad)

渲染流水线大总结

image.png

  1. 渲染进程将HTML内容转换为能够读懂的DOM树结构。
  2. 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。
  3. 创建布局树,并计算元素的布局信息。
  4. 对布局树进行分层,并生成分层树
  5. 为每个图层生成绘制列表,并将其提交到合成线程。
  6. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  7. 合成线程发送绘制图块命令DrawQuad给浏览器进程。
  8. 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上。

重绘和重排

思考

如果下载CSS文件阻塞了,会阻塞DOM树的合成吗?会阻塞页面的显示吗?

不会阻塞DOM树的生成,因为浏览器在解析html时,发现<link rel=”styleSheet” … > 时会并行下载css文件,DOM解析会继续进行。

会阻塞渲染树的生成,构建渲染树需要:DOM Tree + CSSOM Tree → Render Tree,而CSSOM需要等待css文件下载解析后才能生成。只要CSSOM未完成,浏览器就不会构建渲染树,也不会Layout、Paint。

所以CSS会阻塞页面的显示,另外js脚本也会等css准备好后执行,以防js读取不到正确的样式。

不是所有的css文件都会阻塞页面渲染,如果css文件声明的设备不匹配当前设备,例如<link rel="stylesheet" href="print.css" media="print"> ,浏览器会忽略。

如何避免CSS阻塞渲染?

1、使用preload

<link rel="preload" href="style.css" as="style" onload="this.rel='stylesheet'">

特点:

  • 不阻塞渲染
  • 加载优先级高
  • 使用完成后变成真实 CSS

适合:首屏 CSS 较少但不想阻塞

2、内联Critical CSS (关键渲染路径优化)

将首屏关键样式内联:

1
2
3
4
<style>
  /* 关键渲染样式 */
</style>
<link rel="stylesheet" href="rest.css">

流程:

  1. 先渲染首屏
  2. 再异步加载剩余 CSS

适合:SSR 项目、性能优化要求较高的 SPA。

3、拆分CSS-按路由lazy load

特地:不阻塞渲染(dynamic import 下的 CSS 是非阻塞的)。

原理:dynamic import 下的 CSS 本质上是js模块,是被webpack、vite等打包工具打包后,通过js动态创建style或link来引入的,由于是运行时加载,不会影响首屏渲染。

适合:单页应用代码分割。

4、使用 media 测试加载非关键 CSS

1
<link rel="stylesheet" href="mobile.css" media="(max-width: 600px)">

适合:风格切换 / 不同设备样式差异大。

如何优化重绘重排?有哪些实践方法?

渲染进程中的线程 (由ChatGPT整理)

下面给你一份 Chrome(Chromium)渲染进程内部的线程模型全解析,不仅列出线程,还说明它们在整个渲染流水线中的作用、调度方式、何时互相协作,以及哪些工作会触发布局/绘制。

内容非常系统,可以当成参考文档。

🌈 渲染进程(Renderer Process)中的主要线程及职责

现代浏览器渲染进程并非只有“主线程 + Worker”,实际上包含多类专门线程,组合承担整个渲染与执行流水线。下面按照功能分层梳理。


1. 主线程(Main Thread / Render Thread)

主要职责

职责 描述
执行 JavaScript JS 代码解释 + V8 执行
处理 DOM 与 CSSOM DOM 构建、修改,CSS 解析与样式计算
布局(Layout / Reflow) 节点尺寸、位置计算
绘制(Paint)生成 Display List 生成绘制指令(Display List),但不进行光栅化
事件处理 click、keydown、scroll(非合成的)等
微任务与宏任务调度 Promise、setTimeout、requestAnimationFrame
协调合成线程 通知图层变化、提交渲染更新

特点

  • 浏览器页面的绝大多数“逻辑 + 样式 + 布局”都在这一线程执行
  • 如果它被阻塞(长任务 >50ms)就会导致掉帧、卡顿

2. 合成线程(Compositor Thread)

主要职责

职责 描述
管理图层树(Layer Tree) 主线程产生 layer tree 后,合成线程管理其更新
合成操作(Compositing) 将不同图层合成为一帧,尽量不依赖主线程
驱动滚动、惯性滑动 浏览器提供的“线程内滚动”,不阻塞主线程
触发光栅化请求 通知 Raster 线程生成位图
控制 vsync 与帧生成 针对每次 vsync 提交新帧或复用之前的帧

特点

  • 只要图层内容不变(例如 transform/opacity 动画),合成线程即可独立生成新帧
  • 因此合成动画可达到 60/120 FPS,不被 JS 阻塞

举例:

1
2
3
div {
  transition: transform 0.3s;
}

transform 动画会在合成线程执行,不走主线程布局/绘制。


3. Raster 线程 / 光栅化线程(Raster Worker Threads)

主要职责

职责 描述
将 Display List 光栅化为位图(bitmap) 实际生成像素图层
GPU 加速光栅化 在 GPU 进程中使用 Skia + GPU
图块化(Tiling) 页面图层被分割成 tiles,按需光栅化
后台并行 多线程加速,主线程不阻塞

特点

  • 通常是 多个线程,并行 raster(特别是大页面、复杂场景)
  • 现代 Chrome 会使用 GPU rasterization,有性能优势

4. I/O 线程(在渲染进程中也存在)

主要用于:

功能 描述
网络 I/O 不是网络进程,但某些资源/IPC 在此派发
IPC 通信 与浏览器进程、GPU 进程通信
数据流调度 如 streaming、解码任务的派发

I/O 线程避免了主线程直接处理大量系统事件。


5. Image Decode Thread / 解码线程

用于:

  • 图像解码(JPEG / PNG / WebP / AVIF)
  • 部分视频帧解码
  • 解码后的 bitmap 传递给 raster

现代浏览器会尽量使用 GPU 解码,但仍需要 CPU 解码线程。


6. Script Threads:Web Worker、Service Worker、Shared Worker

虽然都是“worker”,但本质是 渲染进程中的独立线程

Dedicated Worker(最常见的 Worker)

  • 由页面创建
  • 一个 worker → 一个独立 JS 线程
  • 不能访问 DOM
  • 用于计算任务

Shared Worker

  • 跨多个页面共享
  • 仍在渲染进程中
  • 有独立事件循环

Service Worker

  • 在浏览器调度下运行
  • 实际执行线程仍属于渲染进程
  • 主要做网络代理、缓存、push等后台业务

Worker 的 JS 执行不会阻塞主线程,但会与主线程竞争 CPU。


7. V8 Helper Threads(V8 内部辅助线程)

V8 引擎内部有多个辅助线程:

名称 功能
编译线程(Compiler Thread) 后台编译/优化 JavaScript
GC 线程(Garbage Collection helpers) 并行或增量垃圾回收
Profiler / Sampling Thread 分析性能

这些线程也运行在渲染进程内部,但属于 V8 私有。


8. Task Scheduler(渲染进程调度器)

Chromium 有自己的任务调度器(Blink Task Scheduler),管理:

  • 主线程任务
  • 合成任务
  • Worker 任务
  • V8 编译任务
  • 性能优先级(用户输入优先、渲染优先、后台任务等)

保证 UI 交互优先级最高,不被后台 worker 任务拖垮。


📌 总结:渲染进程中的完整线程列表

按功能分类如下(最清晰版本):

核心线程

  • 主线程(Main Thread)
  • 合成线程(Compositor Thread)
  • Raster 线程(Raster Worker Threads)

辅助执行线程

  • Web Worker 线程
  • Service Worker 线程
  • Shared Worker 线程

解码 / 绘制支持线程

  • Image Decode Thread
  • Video Decode Thread(可在 GPU 进程也有)

V8 内部线程

  • JIT 编译线程
  • GC 线程
  • Profiler 线程

系统 / IPC 相关

  • I/O Thread
  • Scheduler Threads

📌 最简记忆版(你只需要记住这 6 个关键线程):

线程 核心作用
主线程 JS + DOM + CSS + Layout + Paint
合成线程 独立负责图层合成(Transform/Opacity 动画)
Raster 线程 光栅化生成 bitmap
Worker 线程 主线程卸载计算任务
Image Decode 线程 解码图片
I/O / IPC 线程 进程通信和事件派发