HTML/CSS/JS 是如何变成页面的?
作为前端开发者,你每天都在写 HTML、CSS、JS,但你是否想过:这些纯文本代码,是如何被浏览器转化为可视化页面的?为什么有时候修改 CSS 会卡顿?为什么 JS 执行会阻塞渲染? 本文会从 Chrome 浏览器的底层渲染流程出发,拆解 HTML/CSS/JS 渲染页面的完整链路,结合 DOM 树、CSSOM 树、渲染树、回流重绘等核心概念,不仅讲清楚 “怎么渲染”,还会结合语义化、性能优化等实战知识点,让你既懂原理,又会落地。
一、先搞懂:浏览器渲染的核心目标
浏览器接收 HTML/CSS/JS 等文本输入,最终输出 “可视化页面”,核心目标是:
- 把无序的文本代码,转化为结构化的树状模型;
- 快速、流畅地绘制像素到屏幕(理想状态下 1 秒绘制 60 次,即 60fps,保证视觉流畅);
- 尽可能降低渲染开销,避免卡顿(这也是性能优化的核心)。
Chrome 浏览器的渲染工作主要由渲染引擎(Blink) 负责,整个流程看似复杂,实则有明确的执行顺序 —— 理解这个顺序,是前端性能优化的基础。
二、浏览器渲染页面的完整流程(核心)
从输入 HTML/CSS/JS 到输出页面,Chrome 会经历以下 6 个核心阶段,缺一不可:
下面我们逐阶段拆解,讲清楚每个步骤的核心工作和实战意义。
2.1 第一步:解析 HTML → 构建 DOM 树
浏览器无法直接处理 HTML 字符串,必须先将其转化为结构化的 DOM 树(Document Object Model) —— 这是页面渲染的 “骨架”。 核心过程:
- 字节 → 字符:浏览器先将下载的 HTML 字节(如 UTF-8 编码)转化为可读字符;
- 字符 → 标记:解析字符,识别 HTML 标签、属性、文本等标记;
- 标记 → 节点:为每个标记创建 “节点对象”(包含标签名、属性、文本、父子关系等);
- 节点 → DOM 树:按 HTML 的嵌套关系,递归构建树状结构(根节点是document)。
举个例子:
<!DOCTYPE html>
<html>
<body>
<h1>Hello</h1>
<p>World</p>
</body>
</html>
构建后的 DOM 树简化结构:
document
└── html
└── body
├── h1 (文本:Hello)
└── p (文本:World)
实战意义:HTML 语义化的核心价值 DOM 树的结构完全依赖 HTML 的书写方式,语义化的 HTML 不仅让 DOM 树更清晰,还有两大核心作用:
- SEO 优化:搜索引擎爬虫(如百度蜘蛛)会解析 DOM 树,语义化标签(header/main/footer/section等)能让爬虫快速识别页面核心内容,提升搜索排名;
- 性能与体验:
- 语义化标签(如h1-h6)自带默认的 DOM 结构优先级,主内容用main、侧边栏用aside,浏览器会优先解析 / 加载主内容;
- 配合 CSS 的flex: order(如order: -1让main优先渲染),可进一步优化内容加载顺序。
👉 开发建议:避免用div通吃所有布局,优先使用header/main/footer/nav/article等语义化标签,ul>li代替纯div实现列表,h1-h6按层级使用(一个页面仅一个h1)。
2.2 第二步:解析 CSS → 构建 CSSOM 树
和 HTML 一样,浏览器也无法直接处理 CSS 字符串,需要转化为CSSOM 树(CSS Object Model) —— 这是页面渲染的 “样式规则集”。 核心过程:
- 解析 CSS 文本,识别选择器(如div、.box、#id)、属性(如color、font-size)和值;
- 计算样式优先级(!important > 内联样式 > ID 选择器 > 类选择器 > 标签选择器 > 继承);
- 构建树状结构的 CSSOM,每个节点对应 DOM 节点的样式规则(包含继承的样式)。
举个例子:
body { font-size: 16px; }
h1 { color: red; font-size: 2em; }
构建后的 CSSOM 树简化结构:
CSSOM
├── body
│ └── font-size: 16px
└── h1
├── color: red
├── font-size: 32px (2em = 2*16px)
└── 继承自body:font-size(被自身覆盖)
关键特性:CSSOM 树是 “阻塞渲染” 的 浏览器必须等待所有 CSS 解析完成并构建 CSSOM 树后,才能进入下一步 —— 因为 CSS 样式会影响节点的布局和绘制,未解析完 CSS 就渲染,会导致页面 “闪屏”(先显示无样式内容,再加载样式)。
2.3 第三步:结合 DOM+CSSOM → 构建渲染树(Render Tree)
渲染树是 DOM 树和 CSSOM 树的 “结合体”,但并非简单拼接,核心规则:
- 只包含可见的 DOM 节点(隐藏节点如display: none、标签、meta标签等不会加入渲染树);
- 为每个可见节点绑定对应的 CSSOM 样式规则(继承 + 自身样式);
- 保持 DOM 树的层级结构,确保样式能正确继承。 举个例子(延续上文 DOM/CSSOM): 渲染树结构:
Render Tree
├── body (font-size: 16px)
├── h1 (color: red, font-size: 32px)
└── p (font-size: 16px, 继承自body)
区别:visibility: hidden的节点会加入渲染树(占据空间但不可见),display: none的节点不会加入(不占据空间,完全隐藏)。
2.4 第四步:布局(Layout / 重排):计算节点的位置和大小
渲染树构建完成后,浏览器会执行 “布局” 阶段(也叫 “重排 / 回流”):
- 从根节点开始,递归计算每个节点的盒模型属性:宽度、高度、margin、padding、border、位置(top/left 等);
- 结合视口(viewport)大小,确定每个节点在屏幕上的精确坐标;
- 输出 “布局流”(Layout Flow),记录所有节点的几何信息。 触发布局的常见操作:
- 修改 DOM 节点的尺寸(width/height)、位置(top/left);
- 增删 DOM 节点;
- 修改窗口大小(resize);
- 修改字体大小、padding/margin;
- 调用offsetWidth、scrollTop等获取布局属性的 API(会强制浏览器提前布局)。
2.5 第五步:绘制(Paint / 重绘):填充像素
布局完成后,浏览器进入 “绘制” 阶段:
- 按渲染树的层级,为每个节点绘制像素内容(颜色、背景、边框、阴影、文本等);
- 绘制操作会被拆分为多个 “绘制层”(如文字层、背景层),提升绘制效率;
- 输出 “绘制列表”(Paint List),记录每个绘制层的绘制指令。 触发绘制的常见操作:
- 修改节点的颜色、背景、阴影、边框样式(不改变尺寸 / 位置);
- 注意:绘制不会改变节点的几何信息,开销比布局小。
2.6 第六步:合成(Composite):分层渲染到屏幕
这是渲染的最后一步,浏览器会将所有绘制层合并为最终的页面,并展示到屏幕上:
- 将绘制层按层级叠加(如 z-index 高的层覆盖低的层);
- 利用 GPU 加速合成(GPU 擅长处理分层渲染);
- 将合成后的图像发送到显示器,完成最终渲染(理想状态下 60fps,即每 16.6ms 刷新一次)。 性能优化关键:优先触发合成,避免布局 / 绘制 修改transform(如translate)、opacity等属性,只会触发 “合成” 阶段(开销最小),不会触发布局 / 绘制 —— 这是移动端动画优化的核心技巧。
三、JS 如何影响页面渲染?
JS 是 “动态修改页面” 的核心,但它也是渲染流程中最容易 “阻塞” 的环节,核心原因:
3.1 JS 的执行会阻塞 DOM 解析
浏览器解析 HTML 时,若遇到
- 下载 JS 文件(若为外部脚本);
- 执行 JS 代码(JS 可修改 DOM/CSSOM,如document.createElement、element.style.color);
- 执行完成后,恢复 DOM 解析。 为什么要阻塞? JS 可能会修改已解析的 DOM 节点,甚至重写整个文档(如document.write),若不阻塞 DOM 解析,会导致 JS 操作的节点还未被解析,引发错误。
3.2 JS 可修改 CSSOM,导致重新布局 / 绘制
JS 不仅能修改 DOM,还能修改 CSS 样式(如element.style.fontSize = '20px'),会触发:
- CSSOM 重新计算;
- 渲染树重新构建;
- 布局(重排)→ 绘制(重绘)→ 合成(视情况)。
3.3 优化 JS 渲染阻塞的方案
- 使用async/defer: 1.async:异步下载 JS,下载完成后立即执行(不保证顺序,不阻塞 DOM 解析); 2.defer:异步下载 JS,DOM 解析完成后执行(保证顺序,适合依赖 DOM 的脚本);
- 将 JS 放在底部:先完成 DOM 解析,再执行 JS,避免阻塞首屏渲染;
- 避免频繁修改 DOM/CSS:批量修改样式(如用class代替直接修改style),减少布局 / 绘制次数;
- 使用requestAnimationFrame:将 DOM/CSS 修改放在浏览器重绘前执行,避免多次布局。