浏览器渲染机制详解(包含渲染流程、树结构、异步js)

公众号:小博的前端笔记

一:渲染机制

核心目标: 将 HTML、CSS、JavaScript 代码转换为用户可见、可交互的像素点。

关键渲染路径: 浏览器完成页面首次渲染所必须经过的一系列步骤。优化关键渲染路径是提升页面加载性能的关键。

主要阶段:

  1. 解析(Parsing)与构建 DOM / CSSOM 树:

    • 输入: 接收 HTML 和 CSS 字节流。

    • 过程:

      • 词法分析: 将字节流分解成有意义的标记(Token)。
      • 语法分析: 根据 HTML/CSS 语法规则,将这些标记构建成树形结构。
    • 输出:

      • DOM(Document Object Model)树: HTML 的树形结构表示。每个 HTML 元素(标签、属性、文本)都是树中的一个节点(Node)。DOM 树捕获了文档的内容和结构。
      • CSSOM(CSS Object Model)树: CSS 的树形结构表示。每个 CSS 规则及其包含的选择器和声明都被映射到树中。CSSOM 树捕获了应用于文档的样式信息。
    • 关键点:

      • 渐进式构建: 浏览器在接收到部分 HTML/CSS 后就会开始解析和构建,无需等待全部下载完成。

      • 阻塞行为:

        • <link rel="stylesheet"> 会阻塞渲染树的构建(Render Tree Construction)和首次渲染(Painting),但通常不会阻塞 DOM 的构建(现代浏览器)。 浏览器需要完整的 CSSOM 来计算样式。因此,它会阻塞后续的渲染步骤,直到 CSSOM 构建完成。
      • 优化提示:

        • 将 CSS 放在文档头部 (<head>),尽早开始 CSSOM 构建。
        • 将 JavaScript 放在文档底部 (</body> 前),或者使用 defer/async 属性来避免阻塞 DOM 构建。
        • 避免使用 @import 在 CSS 中引入其他 CSS 文件,因为它会增加 CSSOM 构建的延迟。
        • 使用媒体查询 (media="print", media="(max-width: 600px)") 让非关键 CSS 不阻塞渲染。
  2. 构建渲染树(Render Tree / Frame Tree):

    • 输入: DOM 树 + CSSOM 树。

    • 过程:

      • 浏览器将 DOM 树和 CSSOM 树合并。
      • 遍历可见的 DOM 节点(不包括 headmetascript[type="text/javascript"]display: none 的元素及其后代等),并为每个需要视觉渲染的节点找到匹配的 CSSOM 规则。
      • 应用这些样式规则,生成一棵只包含可见内容 及其计算样式的树------渲染树。
    • 输出: 渲染树。渲染树节点通常称为"渲染对象"或"框"。

    • 关键点:

      • 渲染树只包含需要绘制到屏幕上的元素。
      • visibility: hidden 的元素会包含在渲染树中(因为它占据空间),而 display: none 的元素则不会。
      • 每个渲染对象知道如何将自己及其子元素绘制出来。
  3. 布局(Layout) / 重排(Reflow):

    • 输入: 渲染树。

    • 过程:

      • 计算渲染树中每个节点在视口(Viewport) 内的确切位置(x, y 坐标)和大小(宽度、高度)。这是一个递归过程。
      • 确定所有元素的几何信息(位置、尺寸)。
    • 输出: 每个渲染对象的精确坐标和尺寸信息。

    • 关键点:

      • "流式布局模型": 网页通常采用流式布局,大多数元素的位置是根据其兄弟元素和父元素的位置计算出来的。

      • 全局布局 vs 增量布局: 首次构建渲染树后的布局称为"全局布局"。之后对渲染树的修改(如改变元素尺寸、位置、添加/删除元素)可能只触发受影响部分的"增量布局"(Reflow)。

      • 性能消耗大: 布局是计算密集型操作,频繁触发布局(重排)会严重影响性能。

      • 触发重排的操作示例:

        • 添加、删除、移动 DOM 元素。
        • 改变元素尺寸(width, height, padding, border, margin)。
        • 改变元素位置(position, top, left)。
        • 改变窗口大小(resize 事件)。
        • 改变字体大小或内容(如文本输入框输入文字)。
        • 激活 CSS 伪类(如 :hover 导致尺寸变化)。
        • 读取某些布局属性(如 offsetWidth, offsetHeight, getComputedStyle())会强制浏览器执行同步布局(也称为"强制同步布局"或"布局抖动"),以获得最新值。
  4. 绘制(Painting) / 栅格化(Rasterization):

    • 输入: 经过布局计算的渲染树。

    • 过程:

      • 绘制(Painting): 将每个渲染对象分解成多个绘制指令(如"画矩形"、"画文字"、"画边框"、"画背景")。这是一个记录绘制步骤的过程,发生在主线程。

      • 栅格化(Rasterization): 将绘制指令实际执行 ,转换成屏幕上的像素点 。这个过程通常由合成线程(Compositor Thread) 将任务分发给栅格化线程(Raster Threads) 在 GPU 上完成(硬件加速)。

        • 现代浏览器会将页面划分为多个图层(Layers)。
        • 每个图层独立栅格化(通常是在单独的栅格化线程中)。
        • 图层内容发生变化时(如动画),只需重新栅格化该图层,而不是整个页面。
    • 输出: 内存中的位图(Bitmap),表示页面特定区域的像素数据。

    • 关键点:

      • 重绘(Repaint): 当元素的外观(颜色、背景色、边框颜色、阴影等)改变,但不影响其几何属性(位置、大小)时,浏览器会触发重绘。重绘不一定会触发重排(Layout),但重排一定会触发重绘(因为布局改变后外观通常也需要更新)。

      • 性能消耗: 绘制和栅格化也是消耗性能的操作,但通常比重排轻量。复杂的 CSS 效果(如阴影、渐变)会增加绘制时间。

      • 分层(Layering)与合成(Composition):

        • 浏览器会根据 CSS 属性(如 transform, opacity, will-change, position: fixed, video 元素)将元素提升到独立的合成层(Compositing Layer)
        • 每个图层被栅格化后存储在 GPU 内存中。
        • 合成(Composition): 合成线程负责收集所有图层(称为"合成器帧"),计算它们在视口中的最终位置(考虑滚动、缩放、变换),并将它们合成为一个最终的屏幕图像帧。这个过程非常高效,因为它主要是在 GPU 上操作位图。
  5. 显示(Display):

    • 过程: 合成线程将最终合成的位图帧提交给 GPU,由 GPU 将其扫描输出到显示器上。

    • 关键点:

      • 显示器通常以固定频率(如 60Hz,即每秒 60 次)刷新。
      • 浏览器会尽量将新的合成帧与显示器的刷新周期(VSync)同步,以实现流畅的动画和滚动效果。这就是 requestAnimationFrame API 的意义所在。

流程图总结:

scss 复制代码
字节流 (HTML/CSS)
      ↓
解析 (Parsing)  → [JS 执行可能阻塞 DOM 构建]
      ↓
构建 DOM 树     构建 CSSOM 树 → [CSS 阻塞渲染树构建]
      ↘           ↙
      构建渲染树 (Render Tree) → [只含可见节点+计算样式]
             ↓
           布局 (Layout / Reflow) → [计算精确位置/尺寸]
             ↓
           绘制 (Painting) → [生成绘制指令列表]
             ↓
         栅格化 (Rasterization) → [GPU上执行绘制指令,生成图层位图] (通常在合成线程/栅格线程)
             ↓
          合成 (Composition) → [合成图层,形成最终帧]
             ↓
           显示 (Display) → [GPU输出到屏幕]

延伸点/优化点:

  1. 重排(Reflow)与重绘(Repaint)的优化:

    • 避免触发重排:

      • 避免频繁操作 DOM(使用 DocumentFragment 或离线 DOM 进行批量修改)。
      • 避免逐项修改样式,使用 classcssText 一次性修改。
      • 避免在循环中读取会触发重排的布局属性(offsetTop, offsetLeft, offsetWidth, offsetHeight, scrollTop, scrollLeft, scrollWidth, scrollHeight, clientTop, clientLeft, clientWidth, clientHeight, getComputedStyle())。如果必须读取,先将它们缓存起来。
      • 对复杂动画元素使用绝对定位 (position: absolutefixed),使其脱离文档流,影响范围缩小。
    • 利用合成(Composition)优化动画: 优先使用 transform (位移、缩放、旋转) 和 opacity 属性来制作动画。这些属性可以由合成器线程直接在 GPU 上处理,跳过主线程的布局和绘制阶段,性能极高。

    • will-change 属性: 提示浏览器哪些元素可能会发生变化(如 transform, opacity),让浏览器提前为其创建独立的合成层,优化后续变化的性能(但要谨慎使用,滥用会增加内存消耗)。

    • content-visibility: auto 现代 CSS 属性,可以跳过屏幕外内容的渲染(布局和绘制),大幅提升长页面加载和滚动性能。

  2. 脚本加载优化:

    • defer: 脚本异步下载,在 DOM 解析完成之后、DOMContentLoaded 事件之前按顺序执行。不阻塞 DOM 构建
    • async: 脚本异步下载,下载完成后立即执行(可能在 DOM 解析完成之前或之后)。执行时会阻塞 HTML 解析。适用于无依赖、不操作 DOM 的独立脚本。
    • 模块脚本 (<script type="module">): 默认具有 defer 行为。
  3. 现代渲染架构(Chromium 为例):

    • 主线程(Main Thread / Renderer Thread): 处理 HTML 解析、DOM 构建、CSS 解析、CSSOM 构建、JS 执行(大部分)、布局(Layout)、绘制(Painting - 生成指令列表)。
    • 合成线程(Compositor Thread): 负责图层管理、滚动处理、动画处理(处理 transform/opacity 的合成层动画)、将图层分块(tiles)分发给栅格线程、合成最终帧。
    • 栅格线程(Raster Threads): 在 GPU 上执行绘制指令,将图层分块栅格化为位图。
    • GPU 进程(GPU Process): 管理 GPU 资源,最终将合成线程提交的帧绘制到屏幕上。

回答技巧:

  1. 结构化清晰: 按阶段(解析、构建树、布局、绘制、合成、显示)一步步讲清楚。
  2. 突出关键点: 强调阻塞行为(JS 阻塞 DOM 构建,CSS 阻塞渲染树构建)、重排重绘的区别与优化、合成(Composition)的优势。
  3. 联系实际: 解释为什么要把 CSS 放头部、JS 放底部?为什么 transform 动画更高效?什么是强制同步布局?
  4. 提及优化策略: 主动说出常见的优化手段(如避免重排、使用 transform/opacitydefer/async)。
  5. 了解核心概念: 确保理解 DOM、CSSOM、渲染树、图层(Layer)、合成(Composition)、重排(Reflow)、重绘(Repaint)等术语的含义。
  6. 结合浏览器架构(加分项): 如果深入,可以提一下主线程、合成线程、栅格线程的分工。

二:css树和dom树哪个先构建

核心结论:

DOM树和CSSOM树是并行构建的,但CSSOM的完成会阻塞渲染树构建,而同步JavaScript会阻塞DOM构建

详细构建流程:

关键规则解析:

  1. 并行启动

    • 浏览器同时开始解析HTML构建DOM树和下载/解析CSS构建CSSOM树

    • 示例时间线:

      makefile 复制代码
      0ms: 开始解析HTML → 启动DOM构建
      5ms: 发现<link> → 启动CSS下载
      10ms: 发现<img> → 启动图片下载(非阻塞)
      20ms: CSS下载完成 → 启动CSSOM构建
  2. 阻塞关系

    • CSSOM构建

      • 不会阻塞DOM树的构建(现代浏览器)
      • 但会阻塞渲染树构建(必须等待CSSOM完成)
      • 会阻塞后续JavaScript执行(JS可能依赖样式)
    • JavaScript

      • 同步脚本(<script>)会立即阻塞DOM构建
      • 遇到脚本时,必须等待当前所有CSS下载完成才执行(避免JS操作未解析的样式)
  3. 特殊场景

    xml 复制代码
    <head>
      <link href="style.css" rel="stylesheet"> <!-- 并行构建CSSOM -->
      <script>
        // 此脚本需等待style.css下载完成才执行!
        console.log(getComputedStyle(document.body).color)
      </script>
    </head>
    <body> <!-- DOM构建在此处被脚本阻塞 -->
      <div>内容</div>
    </body>

构建优先级总结:

资源类型 阻塞DOM构建 阻塞渲染树构建 并行性
HTML解析 - - 基础解析流
外部CSS ✅ 并行下载
同步JavaScript ❌ 顺序执行
异步JavaScript ✅ 并行
图片 ✅ 并行下载

最佳答案:

"浏览器会并行启动DOM树和CSSOM树的构建过程,但两者存在关键依赖:

  1. CSS不会阻塞DOM构建:现代浏览器能边下载CSS边构建DOM
  2. CSSOM会阻塞渲染:必须等CSSOM完成才能构建渲染树
  3. JavaScript是最大阻塞源:同步脚本会阻塞DOM构建,且需等待前置CSS下载完成

优化核心:CSS放头部快速构建CSSOM,JavaScript放底部或用async/defer避免阻塞DOM构建"

附加考点:

  • 为什么CSS要放头部? 尽早开始CSS下载,避免渲染树构建延迟导致白屏时间过长

  • 为什么JS放底部? 防止阻塞DOM构建,让用户更快看到页面骨架

  • async vs defer 的区别:

    • async:下载完立即执行,可能中断HTML解析
    • defer:下载完等待HTML解析完成后执行

三、DOM树结构和CSS树结构

DOM树和CSSOM树在底层不是普通的JavaScript对象 ,而是浏览器渲染引擎用C++/Rust等系统语言实现的高度优化的专用数据结构。下面从底层实现角度详细解析:

DOM树的对象结构

核心实现原理:

kotlin 复制代码
// 以Chromium的Blink引擎为例(简化版C++)
class Node {
  NodeType type;          // 节点类型(元素/文本等)
  Node* parent;           // 父节点指针
  Node* first_child;      // 首子节点指针
  Node* next_sibling;     // 兄弟节点指针
  AtomicString node_name; // 节点名称(如"div")
};
​
class Element : public Node {
  HashMap<AtomicString, AtomicString> attrs; // 属性键值对
  StylePropertyMap style_properties;         // 内联样式
  ComputedStyle* computed_style = nullptr;    // 计算后的样式
};
​
class Text : public Node {
  String data; // 文本内容
};

在JavaScript中的表现:

javascript 复制代码
// 浏览器暴露给JS的DOM对象只是底层对象的包装器
const div = document.createElement('div');
​
// 实际内存结构:
   JavaScript Heap
      ↓
   [JS Wrapper Object] ← 通过V8引擎绑定
        │
        └──→ [C++ Node对象] ← Blink引擎内存
               │
               ├── type: ELEMENT_NODE
               ├── tag: "DIV"
               ├── parent: <body的C++对象指针>
               └── style: <指向CSSOM的StylePropertyMap>

🔥 关键真相

  1. 你操作的 div 只是系统对象的JS代理
  2. 每次访问 div.style 都会触发跨引擎通信(JS引擎 ↔ 渲染引擎)
  3. DOM操作昂贵的原因:跨越语言边界 + 触发渲染管线更新

CSSOM树的对象结构

底层实现(以Blink为例):

kotlin 复制代码
// CSS规则存储结构
class CSSRule {
  CSSStyleSheet* parent_sheet;
  Vector<CSSSelector> selectors; // 选择器列表
  StylePropertySet properties;   // 样式键值对
};
​
// 样式表结构
class CSSStyleSheet {
  Vector<CSSRule> rules;
  bool disabled;
};
​
// 计算样式缓存(每个元素独立)
class ComputedStyle {
  const CSSValue* getProperty(CSSPropertyID id) const;
  // 存储最终计算值,如:
  // width: 100px → 实际像素值
  // color: red → RGBA数值
};

JS访问时的表现:

ini 复制代码
// 浏览器暴露的有限接口
const stylesheet = document.styleSheets[0];
const rule = stylesheet.cssRules[0];
​
// 底层实际结构:
   JavaScript访问层
      ↓
   [CSSStyleSheet JS对象] ← 受限代理
        │
        └──→ [C++ CSSStyleSheet对象]
               │
               ├── rules: [C++ CSSRule列表]
               └── 无法直接访问底层样式计算缓存

与普通JS对象的本质差异

特性 普通JS对象 DOM/CSSOM对象
存储位置 JS引擎堆内存 渲染引擎专用内存
操作成本 纳秒级 微秒~毫秒级(涉及跨进程通信)
内存结构 无序属性表 树形结构+样式继承链
修改代价 仅影响JS内存 可能触发重排/重绘
属性访问 直接内存读取 可能触发引擎间IPC通信
垃圾回收 V8引擎管理 跨引擎协同回收

性能陷阱示例

ini 复制代码
// 看似简单的操作,实际发生的事:
for(let i=0; i<1000; i++) {
  div.style.width = i + 'px'; 
}
​
// 底层代价:
1. JS → C++: 1000次跨引擎通信
2. 每次修改: 
   - 更新CSSOM计算样式
   - 检查是否触发重排(这里是会触发的!)
   - 重新计算布局
3. 可能触发1000次重排!

高级调试技巧

在Chrome DevTools中验证:

ini 复制代码
# 1. 访问原生函数(非JS包装器)
> document.body.appendChild.toString()
< "function appendChild() { [native code] }"
​
# 2. 查看隐藏的底层引用
> const div = document.createElement('div');
> %DebugPrint(div);  # 需启用--allow-natives-syntax flag
< 0x1e3e25c0aed9: [JSObject]
   - map=0x1e3e25d82ed1 [FastProperties]
   - prototype=0x1e3e25d0b111
   - elements=0x1e3e25d82671 [HOLEY_ELEMENTS]
   - embedder fields: 2
   - backing_store=0x55aabbcc3340  # ← 指向C++内存的指针!

终极总结

"DOM树和CSSOM树不是JavaScript对象,而是:

  1. 浏览器内核实现的C++树结构(如Blink中的Node/ComputedStyle)
  2. JS对象只是底层对象的跨语言代理(通过V8绑定实现)
  3. 每次访问 element.style 都涉及 JS引擎 ↔ 渲染引擎的IPC通信

这解释了:

  • 为什么DOM操作比JS对象慢100倍+
  • 为什么前端框架要用虚拟DOM批量更新
  • 为什么直接操作CSSOM的API受限(性能和安全考虑)"

四:什么是异步js

异步 JavaScript 的核心特征

不阻塞 DOM 解析,允许浏览器继续构建 DOM 树


异步 JS 的 5 种实现方式

1. async 属性(经典异步)

xml 复制代码
<script src="app.js" async></script>
  • 行为

    • 立即并行下载 脚本,下载完成立即中断 HTML 解析执行脚本
    • 执行顺序:不可控(先下载完先执行)
  • 适用场景:独立脚本(如埋点统计、广告加载)

2. defer 属性(延迟异步)

xml 复制代码
<script src="app.js" defer></script>
  • 行为

    • 并行下载脚本,但延迟到 DOM 解析完成后执行DOMContentLoaded 前)
    • 执行顺序:按文档位置顺序执行
  • 适用场景:依赖 DOM 的脚本(如页面初始化逻辑)

3. 动态脚本注入(高级异步)

ini 复制代码
const script = document.createElement('script');
script.src = 'app.js';
document.head.appendChild(script); // 此时开始异步加载
  • 行为

    • 默认具有 async 行为(可通过 script.async=false 改为按顺序执行)
    • 完全不阻塞解析

4. ES6 模块 (type="module")

xml 复制代码
<script type="module" src="app.mjs"></script>
  • 行为

    • 默认具有 defer 行为(延迟到 DOM 解析后执行)
    • 添加 async 属性可转为立即执行模式

5. Web Worker(线程级异步)

arduino 复制代码
const worker = new Worker('task.js'); // 在独立线程运行
  • 行为

    • 在后台线程执行,完全不阻塞主线程渲染

同步 vs 异步 关键区别

特性 同步 JS (<script>) 异步 JS (async/defer/动态加载)
阻塞 DOM 构建 ✅ 立即停止解析 ❌ 不阻塞
执行时机 下载完立即执行 async:下载完立即执行 defer:DOM 解析后
执行顺序 按文档顺序执行 async:乱序 defer:顺序
依赖 DOM 可能操作未解析的 DOM defer可安全操作完整 DOM

黄金答案

"异步 JavaScript 是指 不阻塞 HTML 解析器 的脚本加载方式,包含 5 种实现:

  1. async 属性:并行下载,下载完立即执行(可能中断解析),执行顺序不可控
  2. defer 属性:并行下载,DOM 解析完成后按序执行
  3. 动态脚本注入 :通过 JS 创建的 <script> 默认异步
  4. ES Module (type="module"):默认等效 defer
  5. Web Worker:在独立线程运行,彻底避免阻塞

核心价值:加速首屏渲染,避免白屏时间过长"


⚠️ 常见面试陷阱

问题

xml 复制代码
<head>
  <script async src="A.js"></script>
  <script defer src="B.js"></script>
  <script src="C.js"></script>
</head>

执行顺序是什么? 答案C.jsA.js(如果先下载完) → B.js 解析

  1. 同步脚本 C.js 立即阻塞解析优先执行
  2. async 脚本 A.js 下载完可能抢在 B.js 前执行
  3. defer 脚本 B.js 最后执行(DOM 解析后)

性能优化必记法则

相关推荐
Dragon Wu1 天前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
Jinuss1 天前
Vue3源码reactivity响应式篇之watch实现
前端·vue3
YU大宗师1 天前
React面试题
前端·javascript·react.js
木兮xg1 天前
react基础篇
前端·react.js·前端框架
ssshooter1 天前
你知道怎么用 pnpm 临时给某个库打补丁吗?
前端·面试·npm
IT利刃出鞘1 天前
HTML--最简的二级菜单页面
前端·html
yume_sibai1 天前
HTML HTML基础(4)
前端·html
给月亮点灯|1 天前
Vue基础知识-Vue集成 Element UI全量引入与按需引入
前端·javascript·vue.js
知识分享小能手1 天前
React学习教程,从入门到精通,React 组件生命周期详解(适用于 React 16.3+,推荐函数组件 + Hooks)(17)
前端·javascript·vue.js·学习·react.js·前端框架·vue3
面向星辰1 天前
html音视频和超链接标签,颜色标签
前端·html·音视频