浏览器渲染原理

5 阅读13分钟

浏览器渲染原理

学习目标

完成本章学习后,你将能够:

  • 理解浏览器的多进程架构
  • 掌握从HTML到像素的完整渲染流程
  • 理解重排(Reflow)和重绘(Repaint)的区别
  • 掌握合成层和GPU加速的原理
  • 能够分析和优化页面渲染性能
  • 理解关键渲染路径(Critical Rendering Path)

前置知识

学习本章内容前,你需要掌握:

问题引入

实际场景

假设你正在开发一个电商网站的商品列表页面,用户反馈页面滚动时卡顿,动画不流畅。

// 问题代码:频繁操作DOM导致性能问题
function updateProductList(products) {
  const container = document.getElementById('product-list');
  
  products.forEach(product => {
    const div = document.createElement('div');
    div.className = 'product-item';
    div.style.width = '200px';        // 触发重排
    div.style.height = '300px';       // 触发重排
    div.style.backgroundColor = 'red'; // 触发重绘
    div.innerHTML = product.name;
    container.appendChild(div);        // 触发重排
  });
}

问题分析

  • 每次修改样式都触发重排或重绘
  • 频繁的DOM操作导致浏览器不断重新计算布局
  • 没有利用GPU加速
  • 阻塞主线程,导致页面卡顿

为什么需要理解浏览器渲染原理

理解浏览器渲染原理可以帮助你:

  • 定位性能瓶颈:知道哪些操作会导致性能问题
  • 优化渲染性能:避免不必要的重排和重绘
  • 提升用户体验:实现流畅的60fps动画
  • 合理使用GPU加速:提升动画和滚动性能

核心概念

概念1:浏览器的多进程架构

现代浏览器(如Chrome)采用多进程架构,提高稳定性和安全性。

浏览器的主要进程
浏览器架构
├── 浏览器进程(Browser Process)
│   ├── 负责浏览器界面(地址栏、书签、前进后退)
│   ├── 负责网络请求
│   └── 负责文件访问
│
├── 渲染进程(Renderer Process)⭐ 核心
│   ├── 每个标签页一个独立进程
│   ├── 负责页面渲染
│   ├── 负责JavaScript执行
│   └── 负责事件处理
│
├── GPU进程(GPU Process)
│   ├── 负责3D绘制
│   └── 负责硬件加速
│
└── 插件进程(Plugin Process)
    └── 负责插件运行(如Flash)
渲染进程的线程
/**
 * 渲染进程包含多个线程
 */

// 1. 主线程(Main Thread)⭐ 最重要
// - 解析HTML、CSS
// - 构建DOM树、CSSOM树
// - 执行JavaScript
// - 计算样式
// - 布局(Layout)
// - 绘制(Paint)

// 2. 合成线程(Compositor Thread)
// - 将页面分成图层
// - 栅格化图层
// - 合成最终画面

// 3. 栅格化线程(Raster Thread)
// - 将图层转换为位图

// 4. Worker线程
// - 执行Web Worker代码
// - 不阻塞主线程

为什么要多进程?

// 优势1:稳定性
// 一个标签页崩溃不影响其他标签页

// 优势2:安全性
// 渲染进程运行在沙箱中,无法访问系统资源

// 优势3:性能
// 多核CPU可以并行处理多个标签页

// 劣势:内存占用较大
// 每个进程都有独立的内存空间

概念2:渲染流程(从HTML到像素)

浏览器将HTML、CSS、JavaScript转换为屏幕上的像素,经历以下步骤:

完整渲染流程
HTML → 解析 → DOM树
                ↓
CSS  → 解析 → CSSOM树
                ↓
         DOM + CSSOM → 渲染树(Render Tree)
                ↓
            布局(Layout)计算位置和大小
                ↓
            绘制(Paint)生成绘制指令
                ↓
            合成(Composite)合成图层
                ↓
            显示(Display)显示到屏幕
步骤1:解析HTML构建DOM树
/**
 * HTML解析过程
 */

// HTML文档
const html = `
<!DOCTYPE html>
<html>
  <head>
    <title>示例</title>
  </head>
  <body>
    <div id="app">
      <h1>标题</h1>
      <p>段落</p>
    </div>
  </body>
</html>
`;

// 解析后的DOM树结构
const domTree = {
  type: 'document',
  children: [
    {
      type: 'html',
      children: [
        {
          type: 'head',
          children: [
            { type: 'title', text: '示例' }
          ]
        },
        {
          type: 'body',
          children: [
            {
              type: 'div',
              id: 'app',
              children: [
                { type: 'h1', text: '标题' },
                { type: 'p', text: '段落' }
              ]
            }
          ]
        }
      ]
    }
  ]
};

/**
 * 解析特点:
 * 1. 增量解析:边下载边解析,不需要等待完整HTML
 * 2. 容错性强:遇到错误会尝试修复
 * 3. 遇到<script>会阻塞解析(除非是async/defer)
 */
步骤2:解析CSS构建CSSOM树
/**
 * CSS解析过程
 */

// CSS样式
const css = `
body {
  font-size: 16px;
  color: #333;
}

#app {
  width: 100%;
  padding: 20px;
}

h1 {
  font-size: 24px;
  color: #000;
}
`;

// 解析后的CSSOM树结构
const cssomTree = {
  body: {
    fontSize: '16px',
    color: '#333'
  },
  '#app': {
    width: '100%',
    padding: '20px'
  },
  h1: {
    fontSize: '24px',
    color: '#000'
  }
};

/**
 * CSSOM特点:
 * 1. 阻塞渲染:必须等待CSS加载和解析完成
 * 2. 层叠规则:后面的样式会覆盖前面的
 * 3. 继承:子元素继承父元素的某些属性
 */
步骤3:构建渲染树(Render Tree)
/**
 * 渲染树构建过程
 * 
 * 渲染树 = DOM树 + CSSOM树
 * 只包含可见元素(display:none的元素不在渲染树中)
 */

// DOM树 + CSSOM树 → 渲染树
const renderTree = [
  {
    type: 'body',
    styles: {
      fontSize: '16px',
      color: '#333'
    },
    children: [
      {
        type: 'div',
        id: 'app',
        styles: {
          width: '100%',
          padding: '20px',
          fontSize: '16px',  // 继承自body
          color: '#333'      // 继承自body
        },
        children: [
          {
            type: 'h1',
            styles: {
              fontSize: '24px',
              color: '#000',
              // ... 其他计算后的样式
            },
            text: '标题'
          },
          {
            type: 'p',
            styles: {
              fontSize: '16px',
              color: '#333',
              // ... 其他计算后的样式
            },
            text: '段落'
          }
        ]
      }
    ]
  }
];

/**
 * 不在渲染树中的元素:
 * - display: none
 * - <head>及其子元素
 * - <script>
 * - <meta>
 * 
 * visibility: hidden的元素在渲染树中(占据空间)
 */
步骤4:布局(Layout / Reflow)
/**
 * 布局阶段:计算每个元素的位置和大小
 */

// 渲染树节点 → 布局树节点(带位置和大小信息)
const layoutTree = [
  {
    type: 'body',
    box: {
      x: 0,
      y: 0,
      width: 1920,   // 视口宽度
      height: 1080   // 视口高度
    },
    children: [
      {
        type: 'div',
        id: 'app',
        box: {
          x: 0,
          y: 0,
          width: 1920,      // 100%
          height: 200,      // 根据内容计算
          padding: {
            top: 20,
            right: 20,
            bottom: 20,
            left: 20
          }
        },
        children: [
          {
            type: 'h1',
            box: {
              x: 20,        // padding-left
              y: 20,        // padding-top
              width: 1880,  // 父元素宽度 - padding
              height: 32    // 根据font-size计算
            }
          },
          {
            type: 'p',
            box: {
              x: 20,
              y: 52,        // h1的y + h1的height
              width: 1880,
              height: 24
            }
          }
        ]
      }
    ]
  }
];

/**
 * 布局计算内容:
 * 1. 盒模型:width、height、padding、border、margin
 * 2. 定位:position、top、left、right、bottom
 * 3. 浮动:float、clear
 * 4. Flexbox/Grid布局
 * 5. 文本换行
 */
步骤5:绘制(Paint)
/**
 * 绘制阶段:生成绘制指令列表
 */

// 布局树 → 绘制指令列表
const paintInstructions = [
  // 绘制body背景
  { type: 'drawRect', x: 0, y: 0, width: 1920, height: 1080, color: '#fff' },
  
  // 绘制div背景
  { type: 'drawRect', x: 0, y: 0, width: 1920, height: 200, color: 'transparent' },
  
  // 绘制h1文本
  {
    type: 'drawText',
    text: '标题',
    x: 20,
    y: 20,
    font: '24px sans-serif',
    color: '#000'
  },
  
  // 绘制p文本
  {
    type: 'drawText',
    text: '段落',
    x: 20,
    y: 52,
    font: '16px sans-serif',
    color: '#333'
  }
];

/**
 * 绘制内容:
 * 1. 背景色
 * 2. 背景图
 * 3. 边框
 * 4. 阴影
 * 5. 文本
 * 6. 其他视觉效果
 * 
 * 绘制顺序(从后到前):
 * 1. 背景
 * 2. 边框
 * 3. 内容
 * 4. 轮廓
 */
步骤6:合成(Composite)
/**
 * 合成阶段:将页面分成多个图层,分别栅格化,最后合成
 */

// 图层划分
const layers = [
  {
    id: 'layer-1',
    type: 'document',
    elements: ['body', 'div#app', 'h1', 'p'],
    transform: 'none'
  },
  {
    id: 'layer-2',
    type: 'composited',  // 独立合成层
    elements: ['div.animated'],
    transform: 'translateZ(0)',  // 触发GPU加速
    reason: 'has 3D transform'
  }
];

/**
 * 什么情况下会创建新图层?
 * 
 * 1. 根元素(<html>)
 * 2. position: fixed/sticky
 * 3. transform: translateZ(0) 或 translate3d(0,0,0)
 * 4. will-change: transform/opacity
 * 5. <video>、<canvas>、<iframe>
 * 6. opacity < 1 且有复杂子元素
 * 7. filter、backdrop-filter
 * 8. z-index且position不是static
 */

// 栅格化:将图层转换为位图
const rasterizedLayers = [
  {
    id: 'layer-1',
    bitmap: [/* 像素数据 */],
    tiles: [
      { x: 0, y: 0, width: 256, height: 256, pixels: [/* ... */] },
      { x: 256, y: 0, width: 256, height: 256, pixels: [/* ... */] }
      // ... 更多瓦片
    ]
  }
];

// 合成:将所有图层合成最终画面
const finalFrame = composeLayers(rasterizedLayers);

概念3:重排(Reflow)和重绘(Repaint)

理解重排和重绘是性能优化的关键。

重排(Reflow / Layout)
/**
 * ========================================
 * 概念:重排(Reflow)
 * ========================================
 * 
 * 【定义】
 * 当元素的几何属性(位置、大小)发生变化时,
 * 浏览器需要重新计算元素的几何属性,这个过程叫重排。
 * 
 * 【触发条件】
 * 1. 添加或删除DOM元素
 * 2. 元素位置变化
 * 3. 元素尺寸变化(width、height、padding、border、margin)
 * 4. 内容变化(文本变化、图片大小变化)
 * 5. 页面初始渲染
 * 6. 浏览器窗口大小变化
 * 7. 读取某些属性(offsetTop、scrollTop、clientWidth等)
 * 
 * 【性能影响】
 * 重排的代价很高,因为需要重新计算布局,影响范围可能很大
 */

// ❌ 触发重排的操作
const element = document.getElementById('box');

// 修改几何属性
element.style.width = '200px';      // 触发重排
element.style.height = '300px';     // 触发重排
element.style.padding = '10px';     // 触发重排
element.style.margin = '20px';      // 触发重排
element.style.border = '1px solid'; // 触发重排

// 修改定位
element.style.position = 'absolute'; // 触发重排
element.style.top = '100px';         // 触发重排
element.style.left = '200px';        // 触发重排

// 修改display
element.style.display = 'none';      // 触发重排
element.style.display = 'block';     // 触发重排

// 修改字体
element.style.fontSize = '20px';     // 触发重排(影响文本布局)

// 添加/删除元素
const newElement = document.createElement('div');
document.body.appendChild(newElement); // 触发重排

// 读取布局属性(强制同步布局)
const width = element.offsetWidth;   // 触发重排
const height = element.offsetHeight; // 触发重排
const top = element.offsetTop;       // 触发重排
重绘(Repaint)
/**
 * ========================================
 * 概念:重绘(Repaint)
 * ========================================
 * 
 * 【定义】
 * 当元素的外观(颜色、背景等)发生变化,但几何属性不变时,
 * 浏览器只需要重新绘制元素,这个过程叫重绘。
 * 
 * 【触发条件】
 * 1. 颜色变化(color、background-color)
 * 2. 边框样式变化(border-style、border-color)
 * 3. 可见性变化(visibility)
 * 4. 阴影变化(box-shadow、text-shadow)
 * 5. 背景图变化(background-image)
 * 
 * 【性能影响】
 * 重绘的代价比重排小,但仍然需要重新绘制像素
 */

// ✅ 只触发重绘的操作(不触发重排)
const element = document.getElementById('box');

// 修改颜色
element.style.color = 'red';              // 只重绘
element.style.backgroundColor = 'blue';   // 只重绘

// 修改边框颜色
element.style.borderColor = 'green';      // 只重绘

// 修改可见性
element.style.visibility = 'hidden';      // 只重绘(仍占据空间)

// 修改阴影
element.style.boxShadow = '0 0 10px #000'; // 只重绘

// 修改背景图
element.style.backgroundImage = 'url(...)'; // 只重绘
重排 vs 重绘对比
/**
 * 性能对比
 */

// 重排(Reflow)
// - 需要重新计算布局
// - 影响范围可能很大(父元素、子元素、兄弟元素)
// - 性能开销大
// - 一定会触发重绘

// 重绘(Repaint)
// - 只需要重新绘制像素
// - 影响范围较小(只影响当前元素)
// - 性能开销较小
// - 不一定触发重排

/**
 * 最佳情况:既不重排也不重绘
 * 使用transform和opacity,只触发合成
 */

// ✅ 最佳:只触发合成(Composite)
element.style.transform = 'translateX(100px)'; // 不重排不重绘
element.style.opacity = '0.5';                 // 不重排不重绘

// 原因:transform和opacity在合成层上处理,不影响布局和绘制

概念4:合成层与GPU加速

合成层是浏览器渲染优化的关键。

什么是合成层
/**
 * ========================================
 * 概念:合成层(Composite Layer)
 * ========================================
 * 
 * 【定义】
 * 浏览器将页面分成多个独立的图层,每个图层可以独立绘制和合成。
 * 某些图层会被提升为合成层,由GPU处理。
 * 
 * 【优势】
 * 1. 独立绘制:合成层的变化不影响其他层
 * 2. GPU加速:利用GPU处理transform和opacity
 * 3. 避免重排重绘:某些操作只需要重新合成
 * 
 * 【创建合成层的条件】
 */

// 方法1:3D transform
const element1 = document.getElementById('box1');
element1.style.transform = 'translateZ(0)';     // 创建合成层
element1.style.transform = 'translate3d(0,0,0)'; // 创建合成层

// 方法2:will-change
const element2 = document.getElementById('box2');
element2.style.willChange = 'transform';  // 提示浏览器创建合成层
element2.style.willChange = 'opacity';    // 提示浏览器创建合成层

// 方法3:video、canvas、iframe
const video = document.createElement('video');
// video元素自动创建合成层

// 方法4:position: fixed
const fixed = document.getElementById('fixed');
fixed.style.position = 'fixed';  // 可能创建合成层

// 方法5:opacity < 1 且有复杂子元素
const parent = document.getElementById('parent');
parent.style.opacity = '0.9';  // 可能创建合成层

// 方法6:filter
const filtered = document.getElementById('filtered');
filtered.style.filter = 'blur(5px)';  // 创建合成层
GPU加速的原理
/**
 * GPU加速的属性
 * 
 * 只有以下属性可以利用GPU加速:
 * 1. transform(translate、rotate、scale)
 * 2. opacity
 * 
 * 其他属性都需要CPU处理
 */

// ✅ 使用GPU加速的动画(60fps流畅)
function animateWithGPU() {
  const element = document.getElementById('box');
  
  // 创建合成层
  element.style.willChange = 'transform';
  
  let x = 0;
  function animate() {
    x += 1;
    // 使用transform,GPU处理
    element.style.transform = `translateX(${x}px)`;
    
    if (x < 500) {
      requestAnimationFrame(animate);
    } else {
      // 动画结束,移除will-change
      element.style.willChange = 'auto';
    }
  }
  
  requestAnimationFrame(animate);
}

// ❌ 不使用GPU加速的动画(可能卡顿)
function animateWithoutGPU() {
  const element = document.getElementById('box');
  
  let x = 0;
  function animate() {
    x += 1;
    // 使用left,触发重排,CPU处理
    element.style.left = `${x}px`;
    
    if (x < 500) {
      requestAnimationFrame(animate);
    }
  }
  
  requestAnimationFrame(animate);
}

/**
 * 性能对比:
 * 
 * GPU加速(transform):
 * - 在合成层上处理
 * - 不触发重排和重绘
 * - 60fps流畅
 * 
 * CPU处理(left):
 * - 触发重排
 * - 阻塞主线程
 * - 可能掉帧
 */
合成层的陷阱
/**
 * 陷阱1:层爆炸(Layer Explosion)
 * 
 * 过多的合成层会消耗大量内存
 */

// ❌ 坏:为每个元素创建合成层
const items = document.querySelectorAll('.item');
items.forEach(item => {
  item.style.willChange = 'transform';  // 创建1000个合成层!
});
// 问题:内存占用过高,反而降低性能

// ✅ 好:只为需要动画的元素创建合成层
function animateItem(item) {
  // 动画开始前创建合成层
  item.style.willChange = 'transform';
  
  // 执行动画
  item.style.transform = 'translateX(100px)';
  
  // 动画结束后移除
  item.addEventListener('transitionend', () => {
    item.style.willChange = 'auto';
  }, { once: true });
}

/**
 * 陷阱2:隐式合成
 * 
 * 某些情况下,浏览器会自动创建合成层
 */

// 场景:z-index导致的隐式合成
const layer1 = document.getElementById('layer1');
const layer2 = document.getElementById('layer2');

layer1.style.transform = 'translateZ(0)';  // 创建合成层
layer1.style.zIndex = '1';

// layer2在layer1上面,但没有合成层
// 浏览器会自动为layer2创建合成层(隐式合成)
layer2.style.zIndex = '2';

// 解决方案:显式创建合成层
layer2.style.transform = 'translateZ(0)';

概念5:关键渲染路径(Critical Rendering Path)

关键渲染路径是指浏览器从接收HTML到首次渲染的过程。

关键渲染路径的步骤
/**
 * 关键渲染路径(CRP)
 * 
 * 1. 接收HTML
 * 2. 解析HTML → DOM树
 * 3. 加载CSS
 * 4. 解析CSS → CSSOM树
 * 5. 构建渲染树
 * 6. 布局
 * 7. 绘制
 * 8. 合成
 * 9. 首次渲染(First Paint)
 */

// 阻塞渲染的资源
// 1. CSS:阻塞渲染(必须等待CSS加载和解析)
// 2. JavaScript:阻塞解析(除非使用async/defer)

/**
 * 优化关键渲染路径
 */

// 1. 减少关键资源数量
// ❌ 坏:多个CSS文件
// <link rel="stylesheet" href="style1.css">
// <link rel="stylesheet" href="style2.css">
// <link rel="stylesheet" href="style3.css">

// ✅ 好:合并CSS文件
// <link rel="stylesheet" href="bundle.css">

// 2. 减少关键资源大小
// ✅ 压缩CSS和JavaScript
// ✅ 移除未使用的CSS
// ✅ 使用代码分割

// 3. 缩短关键路径长度
// ✅ 内联关键CSS
// <style>
//   /* 首屏关键样式 */
//   body { margin: 0; }
//   .header { height: 60px; }
// </style>

// ✅ 异步加载非关键CSS
// <link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'">

// ✅ 延迟加载JavaScript
// <script src="app.js" defer></script>
// <script src="analytics.js" async></script>

最佳实践

企业级应用场景

场景1:优化列表渲染性能
// ❌ 坏:频繁触发重排
function badRenderList(items) {
  const container = document.getElementById('list');
  
  items.forEach(item => {
    const div = document.createElement('div');
    div.textContent = item.name;
    div.style.width = '200px';   // 触发重排
    div.style.height = '50px';   // 触发重排
    container.appendChild(div);   // 触发重排
  });
}

// ✅ 好:批量操作,减少重排
function goodRenderList(items) {
  const container = document.getElementById('list');
  const fragment = document.createDocumentFragment();
  
  items.forEach(item => {
    const div = document.createElement('div');
    div.textContent = item.name;
    div.className = 'list-item';  // 使用CSS类
    fragment.appendChild(div);     // 在fragment中操作,不触发重排
  });
  
  container.appendChild(fragment); // 只触发一次重排
}

// CSS
// .list-item {
//   width: 200px;
//   height: 50px;
// }
场景2:优化动画性能
// ❌ 坏:使用left/top动画
function badAnimation() {
  const element = document.getElementById('box');
  element.style.position = 'absolute';
  
  let x = 0;
  setInterval(() => {
    x += 1;
    element.style.left = x + 'px';  // 触发重排
  }, 16);
}

// ✅ 好:使用transform动画
function goodAnimation() {
  const element = document.getElementById('box');
  
  // 提示浏览器创建合成层
  element.style.willChange = 'transform';
  
  let x = 0;
  function animate() {
    x += 1;
    element.style.transform = `translateX(${x}px)`;  // 只触发合成
    
    if (x < 500) {
      requestAnimationFrame(animate);
    } else {
      element.style.willChange = 'auto';  // 移除提示
    }
  }
  
  requestAnimationFrame(animate);
}

// 💡 更好:使用CSS动画
function bestAnimation() {
  const element = document.getElementById('box');
  element.classList.add('animate');
}

// CSS
// .animate {
//   animation: slide 1s ease-out;
// }
// 
// @keyframes slide {
//   from { transform: translateX(0); }
//   to { transform: translateX(500px); }
// }
场景3:避免强制同步布局
// ❌ 坏:强制同步布局(Layout Thrashing)
function badLayout() {
  const elements = document.querySelectorAll('.box');
  
  elements.forEach(element => {
    // 读取布局属性
    const width = element.offsetWidth;  // 触发重排
    
    // 修改样式
    element.style.width = (width + 10) + 'px';  // 触发重排
    
    // 再次读取
    const height = element.offsetHeight;  // 触发重排
    
    // 再次修改
    element.style.height = (height + 10) + 'px';  // 触发重排
  });
  // 问题:读写交替,每次都触发重排
}

// ✅ 好:批量读取,批量写入
function goodLayout() {
  const elements = document.querySelectorAll('.box');
  
  // 第一遍:批量读取
  const sizes = Array.from(elements).map(element => ({
    width: element.offsetWidth,   // 只触发一次重排
    height: element.offsetHeight
  }));
  
  // 第二遍:批量写入
  elements.forEach((element, index) => {
    element.style.width = (sizes[index].width + 10) + 'px';
    element.style.height = (sizes[index].height + 10) + 'px';
  });
  // 只触发一次重排
}

常见陷阱

陷阱1:过度使用will-change
// ❌ 错误:为所有元素添加will-change
const elements = document.querySelectorAll('*');
elements.forEach(el => {
  el.style.willChange = 'transform, opacity';
});
// 问题:创建过多合成层,内存占用过高

// ✅ 正确:只在需要时使用will-change
function animateElement(element) {
  // 动画前添加
  element.style.willChange = 'transform';
  
  // 执行动画
  element.style.transform = 'scale(1.2)';
  
  // 动画后移除
  element.addEventListener('transitionend', () => {
    element.style.willChange = 'auto';
  }, { once: true });
}
陷阱2:在循环中读取布局属性
// ❌ 错误:在循环中读取offsetWidth
function badLoop() {
  const container = document.getElementById('container');
  
  for (let i = 0; i < 1000; i++) {
    const div = document.createElement('div');
    div.style.width = container.offsetWidth + 'px';  // 每次都触发重排
    container.appendChild(div);
  }
}

// ✅ 正确:在循环外读取
function goodLoop() {
  const container = document.getElementById('container');
  const width = container.offsetWidth;  // 只触发一次重排
  
  const fragment = document.createDocumentFragment();
  for (let i = 0; i < 1000; i++) {
    const div = document.createElement('div');
    div.style.width = width + 'px';
    fragment.appendChild(div);
  }
  
  container.appendChild(fragment);
}

性能优化建议

优化1:使用CSS containment
/**
 * CSS containment:限制元素的影响范围
 */

/* 告诉浏览器这个元素的内部变化不影响外部 */
.container {
  contain: layout;  /* 布局隔离 */
  contain: paint;   /* 绘制隔离 */
  contain: size;    /* 尺寸隔离 */
  contain: style;   /* 样式隔离 */
  
  /* 或者使用简写 */
  contain: strict;  /* layout + paint + size */
  contain: content; /* layout + paint */
}

/* 使用场景:独立的组件、卡片、列表项 */
.card {
  contain: content;
}
优化2:使用content-visibility
/**
 * content-visibility:跳过屏幕外元素的渲染
 */

.list-item {
  /* 屏幕外的元素跳过渲染 */
  content-visibility: auto;
  
  /* 设置预估高度,避免滚动条跳动 */
  contain-intrinsic-size: 200px;
}

/* 性能提升:大幅减少初始渲染时间 */
优化3:使用requestAnimationFrame
/**
 * 使用requestAnimationFrame同步动画与浏览器刷新
 */

// ❌ 坏:使用setInterval
function badAnimate() {
  setInterval(() => {
    // 可能在浏览器刷新之间执行多次
    updateAnimation();
  }, 16);
}

// ✅ 好:使用requestAnimationFrame
function goodAnimate() {
  function update() {
    updateAnimation();
    requestAnimationFrame(update);
  }
  requestAnimationFrame(update);
}

// 优势:
// 1. 与浏览器刷新同步(60fps)
// 2. 页面不可见时自动暂停
// 3. 更流畅的动画

实践练习

练习1:分析页面渲染性能(难度:简单)

需求描述

使用Chrome DevTools分析页面的渲染性能,找出性能瓶颈。

步骤

  1. 打开Chrome DevTools
  2. 切换到Performance面板
  3. 录制页面加载过程
  4. 分析FPS、重排、重绘

分析要点

  • FPS是否稳定在60
  • 是否有长任务(Long Task)
  • 重排和重绘的频率
  • 合成层的数量

练习2:优化动画性能(难度:中等)

需求描述

优化以下动画代码,使其达到60fps。

原始代码

// 性能差的动画
function animateBox() {
  const box = document.getElementById('box');
  let x = 0;
  
  setInterval(() => {
    x += 1;
    box.style.left = x + 'px';
    box.style.backgroundColor = `hsl(${x}, 50%, 50%)`;
  }, 16);
}

优化提示

  • 使用transform代替left
  • 使用requestAnimationFrame代替setInterval
  • 创建合成层
  • 避免修改backgroundColor(触发重绘)

参考答案

// 优化后的动画
function animateBox() {
  const box = document.getElementById('box');
  
  // 创建合成层
  box.style.willChange = 'transform';
  
  let x = 0;
  function animate() {
    x += 1;
    
    // 使用transform(只触发合成)
    box.style.transform = `translateX(${x}px)`;
    
    if (x < 500) {
      requestAnimationFrame(animate);
    } else {
      box.style.willChange = 'auto';
    }
  }
  
  requestAnimationFrame(animate);
}

// 如果需要改变颜色,使用CSS动画
// CSS:
// @keyframes colorChange {
//   from { filter: hue-rotate(0deg); }
//   to { filter: hue-rotate(360deg); }
// }
// 
// .box {
//   animation: colorChange 5s linear infinite;
// }

进阶阅读