浏览器渲染全过程解析

深入探索浏览器如何将代码转化为视觉盛宴,揭示高效渲染背后的技术原理

网络线程到主线程

当你在浏览器地址栏输入URL并按下回车时,浏览器网络线程首先接收到HTML文档。这个文档只是一串字符,浏览器需要将其转化为用户可见的像素。网络线程会产生一个渲染任务,并将其传递给渲染主线程的消息队列

渲染起点 -> 网络线程接收HTML文档 -> 创建渲染任务 -> 加入渲染主线程队列

CSS加载为何不阻塞HTML解析?

我们在写html文档时,会将css放入body上面部分,以style或link等方式接入css,不只是为了快速渲染页面样式。

在HTML解析过程中遇到CSS资源时,浏览器会启动一个预解析线程专门处理CSS,生成CSSOM树(CSS对象模型)。这个预解析线程与主渲染线程并行工作,互不干扰。所以早放早运行。

JavaScript阻塞渲染,为什么不也用一个线程?

JavaScript需要直接操作DOM,而DOM树构建是渲染主线程的核心任务。如果JavaScript在单独的线程中执行,会导致复杂的线程同步问题,因此浏览器设计为在渲染主线程中顺序执行JavaScript。简单来说就是没有那个必要。

HTML解析开始 -> 遇到JavaScript -> 暂停HTML解析 -> 下载并执行JS -> 恢复HTML解析

行盒与块盒的奥秘

在CSS中,盒子的类型由display属性决定,而不是HTML元素类型。W3C规定,内容必须包含在行盒中<p>a</p>这在块盒中影响规定,浏览器会自动生成匿名行盒包裹 a。同时,行盒和块盒不能相邻,当行盒和块盒相邻时,浏览器会自动生成匿名盒。

css 复制代码
<div>
  <p>a</p>
  b
  <p>c</p>
</div>

DOM树与布局树的差异

DOM树表示文档结构,而布局树(渲染树)只包含可见元素及其样式信息。两者并非一一对应:

  • display:none节点没有几何信息,所以不会生成到布局树,但是会生成到dom树
  • 伪元素有几何信息,会生成到布局树,但是dom树不存在伪元素节点(css中写的)

不知道伪元素是什么可以看看我之前的文章CSS 伪元素

分层与合成(渲染加速)

为了提高渲染效率,浏览器会将页面分成多个层,独立处理:

主线程会使用自己的策略对布局树分层

  • 为了在某个层改变仅对修改层处理,提升效率
  • 滚动条、堆叠上下文、transform、opacity 等样式都会或多或少影响分层结果
  • 通过will-change属性更大程度的影响分层结果
  • 但也都是影响,还是看浏览器怎么分

百度首页的分层效果:

渲染流水线

浏览器渲染的最后阶段将分层后的内容转化为屏幕上的像素:

绘制

  • 主线程会为每个层单独生成绘制指令集(描述这层怎么画)
  • 然后将每个图层的绘制信息交给合成线程

分块

  • 合成线程会为每个图层生成分块信息

光栅化

  • 合成线程将分块信息给GPU进程,快速完成光栅化
  • GPU进程会开启多个线程来完成光栅化,并优先处理靠近视口区的块
  • 最终呈现一块一块的位图

  • 合成线程计算出每个位图在屏幕上的位置
  • 交给GPU进行最终呈现

为什么交给GPU,而不是自己

1.渲染进程(在沙盒)独立 安全

2.渲染主线程

  • 合成线程(transform也在这,不用参与前面的步骤,效率极高)//滚动也是
  • 渲染进程和操作系统的硬件是隔离的,所以合成线程无法直接交给硬件

transform优势展示

transfrom 直接就走最后像素展示的流程,所以不管你JS怎么折腾(卡死循环)都无济于事。滚动条也是在这里所以卡死循环也不影响页面展示滚动条,大家可以自己试试。

完整代码:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .ball{
            width: 100px;
            height: 100px;
            background: pink;
            border-radius: 50%;
            margin: 30px;
        }
        .ball1{
            animation: move1 1s alternate infinite ease-in-out;
        }
        .ball2{
            position: fixed;
            left: 0;
            animation: move2 1s alternate infinite ease-in-out;
        }
        @keyframes move1{
            to{
                transform: translateX(100px);
            }
        }
        @keyframes move2{
            to{
                left: 100px;
            }
        }
    </style>
</head>
<body>
    <button id="btn">卡你五秒如何呢</button>
    <div class="ball ball1"></div>
    <div class="ball ball2"></div>
    <script>
        function delay(time){
            var start = Date.now();
            while(Date.now() - start < time){}
        }
        document.getElementById('btn').onclick = function(){
            delay(5000);
        }
    </script>
</body>
</html>

重排(Reflow)与重绘(Repaint)

理解重排和重绘对性能优化至关重要,以下是重排重绘的代码简单示例,大家可以自己玩一下:

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <title>重排与重绘</title>
  <style>
    .reflow-demo {
      width: 300px;
      height: 200px;
      border: 2px solid #3498db;
      padding: 15px;
      margin: 20px auto;
      position: relative;
    }
    .box {
      width: 100px;
      height: 100px;
      background: #2ecc71;
      transition: all 0.3s;
    }
    .controls {
      text-align: center;
      margin: 20px 0;
    }
    button {
      padding: 8px 15px;
      margin: 0 5px;
      background: #3498db;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    .console {
      background: #2c3e50;
      color: #ecf0f1;
      padding: 15px;
      font-family: monospace;
      height: 100px;
      overflow-y: auto;
      margin-top: 20px;
    }
  </style>
</head>
<body>
  <div class="reflow-demo">
    <div class="box" id="demoBox"></div>
  </div>
  
  <div class="controls">
    <button onclick="changeSize()">改变尺寸(重排)</button>
    <button onclick="changeColor()">改变颜色(重绘)</button>
    <button onclick="transformBox()">Transform(合成)</button>
  </div>
  
  <div class="console" id="console"></div>
  
  <script>
    const box = document.getElementById('demoBox');
    const consoleEl = document.getElementById('console');
    
    function log(message) {
      consoleEl.innerHTML += `[${new Date().toLocaleTimeString()}] ${message}\n`;
      consoleEl.scrollTop = consoleEl.scrollHeight;
    }
    
    function changeSize() {
      log('开始:改变元素尺寸...');
      const start = performance.now();
      
      // 触发重排
      box.style.width = box.offsetWidth === 100 ? '150px' : '100px';
      
      // 读取布局信息 - 强制同步重排
      const width = box.offsetWidth;
      
      const duration = performance.now() - start;
      log(`完成:尺寸改变 (${duration.toFixed(2)}ms)`);
      log(`读取宽度: ${width}px (强制同步重排)`);
    }
    
    function changeColor() {
      log('开始:改变背景颜色...');
      const start = performance.now();
      
      // 只触发重绘
      box.style.backgroundColor = box.style.backgroundColor === 'rgb(46, 204, 113)' ? 
                                 '#9b59b6' : '#2ecc71';
      
      const duration = performance.now() - start;
      log(`完成:颜色改变 (${duration.toFixed(2)}ms)`);
    }
    
    function transformBox() {
      log('开始:应用transform...');
      const start = performance.now();
      
      // 使用transform只触发合成
      box.style.transform = box.style.transform === 'translateX(50px)' ? 
                           'translateX(0)' : 'translateX(50px)';
      
      const duration = performance.now() - start;
      log(`完成:transform应用 (${duration.toFixed(2)}ms)`);
    }
  </script>
  
  <div class="explanation">
    <p><strong>性能关键点:</strong></p>
    <ul>
      <li>重排(reflow)会重新计算布局 - 代价最高</li>
      <li>重绘(repaint)不改变布局,但需要重新绘制</li>
      <li>transform和opacity只触发合成(composite) - 最高效</li>
      <li>重排是异步的,但读取布局属性会强制同步重排</li>
    </ul>
  </div>
</body>
</html>

reflow 它是异步的,会等js执行完成一起提交给合成线程

但是在读取的时候是同步的(因为读取的时候如果js将其中元素改变,异步的话,读取就会还是原来元素的状态)

总体流程

总结

浏览器渲染是一个复杂而精密的流水线作业,从接收HTML字符串开始,经历解析、样式计算、布局、分层、绘制、光栅化到最终合成,每一步都经过高度优化。理解这个过程有助于开发者:

  1. 优化页面性能:减少重排和重绘,优先使用transform和opacity
  2. 合理组织资源:CSS放头部,JS放底部或使用async/defer
  3. 避免布局抖动:避免在循环中交替读写布局属性
  4. 利用分层优势:合理使用will-change提示浏览器优化渲染
相关推荐
然我2 小时前
防抖与节流:如何让频繁触发的函数 “慢下来”?
前端·javascript·html
鱼樱前端2 小时前
2025前端人一文看懂 Broadcast Channel API 通信指南
前端·vue.js
烛阴2 小时前
非空断言完全指南:解锁TypeScript/JavaScript的安全导航黑科技
前端·javascript
鱼樱前端2 小时前
2025前端人一文看懂 window.postMessage 通信
前端·vue.js
快乐点吧2 小时前
【前端】异步任务风控验证与轮询机制技术方案(通用笔记版)
前端·笔记
pe7er3 小时前
nuxtjs+git submodule的微前端有没有搞头
前端·设计模式·前端框架
七月的冰红茶3 小时前
【threejs】第一人称视角之八叉树碰撞检测
前端·threejs
爱掉发的小李3 小时前
前端开发中的输出问题
开发语言·前端·javascript
Dolphin_海豚3 小时前
一文理清 node.js 模块查找策略
javascript·后端·前端工程化
祝余呀4 小时前
HTML初学者第四天
前端·html