1、浏览器渲染原理
1.1 浏览器的进程模型
1.1.1 进程和线程的概念
进程:
程序运行需要有它自己专属的内存空间,可以把这块内存空间简单地理解为进程。

每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。
线程:
有了进程后,就可以运行程序的代码了,可以简单地理解运行代码的【人】称之为【线程】。
一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程。
如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程。

1.1.2 浏览器有哪些进程和线程?
浏览器是一个多进程多线程的应用程序。
浏览器内部工作极其复杂,为了避免相互影响导致连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。
可以在浏览器右上角设置及其他(Alt+F)-更多工具-浏览器任务管理器查看当前浏览器的进程:

其中,最主要的进程有:
- 浏览器进程:主要负责界面显示(上面标签页的展示,如前进后退按钮、地址栏、书签收藏等)、用户交互、子进程管理(其它的进程如网络进程渲染进程是由浏览器进程产生的)等。浏览器进程内部会启动多个线程处理不同的任务。
- 网络进程:负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。
- 渲染进程(本节课重点讲解的进程):渲染进程启动后,会开启一个渲染主线程,主线程负责执行HTML、CSS、JS 代码。默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响。
1.2 渲染主线程是如何工作的?
渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于: 
思考题:为什么渲染进程不使用多个线程来处理这些事情?
1、DOM 操作的竞态条件问题
2、渲染流程的依赖关系:HTML解析 → CSS解析 → 样式计算 → 布局 → 绘制,后面的步骤依赖前面步骤的结果,无法真正并行。
3、同步成本可能更高:需要大量的锁机制+线程间通信开销+可能比单线程更慢
浏览器实际上采用了其他策略: 1、异步处理:通过事件循环和任务队列; 2、Web Workers; 3、GPU 加速; 4、其他辅助线程:网络请求、文件读写等在其他线程处理。
要处理这么多的任务,主线程遇到了一个前所未有的难题:如何调度任务?
比如:
- 我正在执行一个JS 函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
- 我正在执行一个JS 函数,执行到一半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
- 浏览器进程通知我"用户点击了按钮",与此同时,某个计时器也到达了时间,我应该处理哪一个呢?
- ......
渲染主线程想出了一个绝沙的主意来处理这个问题------排队:

JS事件循环又叫做消息循环,是浏览器渲染主线程的工作方式,是一种让JavaScript既是单线程,又不会阻塞的机制。它负责执行代码、收集和处理事件、执行队列中的子任务。事件队列是一个存放待执行的任务的列表,有宏任务队列和微任务队列之分。
分类任务可以帮助JavaScript引擎在执行过程中进行任务的优化和调度。通过将任务分为微任务和宏任务,JavaScript引擎可以根据执行上下文的不同特点和优先级来合理地安排任务的执行顺序。
微任务是指在当前任务执行结束后立即执行的任务,它可以看作是在当前任务的"尾巴"添加的任务。常见的微任务包括 Promise.then 回调(注意:new Promise在实例化的过程中所执行的代码都是同步进行的)和 MutationObserver回调、nextTick等。
宏任务是指需要排队等待 JavaScript 引擎空闲时才能执行的任务。常见的宏任务包括 setTimeout、setInterval、I/O 操作、DOM 事件等。
通俗地讲,可以将js代码执行需要借助三个通道:主线程,微任务队列,宏任务队列。对于一段需要执行的js代码,从上往下遍历,遇到同步代码便直接放入主线程中依次执行,遇到微任务则放入微任务队列,遇到宏任务则放入微任务队列,此时从微任务队列拿出任务到主线程中执行,执行过程中可能产生另外的微任务/宏任务,按例放入相应的队列中。直到微任务队列为空,开启新的一轮事件循环,再去从宏任务队列中拿出一个任务到主线程中执行,如果产生了新的微任务,则停止宏任务的下一个宏任务的执行,先去清空微任务队列。
同步任务、微任务、宏任务的执行顺序是:先执行同步代码,然后清空微任务队列,再执行一个宏任务,然后重复这个过程。这里容易产生一个错误的认识:就是微任务先于宏任务执行。实际上是先执行同步任务然后在执行异步任务,异步任务是分宏任务和微任务两种的。
如何理解JS的异步?
参考答案:
JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。而渲染主线程承担着诸多的工作,如渲染页面、执行JS 都在其中运行。
如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其它任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新.给用户造成卡死现象。
所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
1.3 浏览器是如何渲染页面的?
当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。
整个渲染流程分为多个阶段,分别是: HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、画。
每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。
这样,整个渲染流程就形成了一套组织严密的生产流水线。
可以着重讲的是渲染的第一步,即解析 HTML。
解析过程中遇到 CSS 解析 CSS,遇到 JS 执行 JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。
如果主线程解析到link位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的根本原因。
如果主线程解析到script位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。
第一步完成后,会得到 DOM 树和 CSSOM 树(CSS Object Model),浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。
经过下一步的样式计算后,会得到一棵带有样式的 DOM 树。
紧接着渲染主线程陆续完成布局、分层、绘制等工作。
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成,合成线程会从线程池中拿取多个线程来完成分块工作。
最后,合成线程会将块信息交给 GPU 进程,会开启多个线程来完成光栅化以极高的速度完成光栅化。
光栅化的结果,就是一块一块的位图,最后呈现在屏幕上。
2、React执行原理
2.1 React代码是怎么运行在浏览器中的?
React代码需要经过一系列转换才能在浏览器中运行,主要包括以下几个关键步骤:
1、JSX 转化
React使用JSX语法,但浏览器无法直接理解JSX。需要通过Babel等转译器将JSX转换为普通的JavaScript:
jsx
// 原始JSX代码
const element = <h1>Hello, World!</h1>;
// 转换后的JavaScript
const element = React.createElement('h1', null, 'Hello, World!');
2、模块打包
使用Webpack、Vite等打包工具将多个模块文件打包成浏览器可以加载的bundle文件:
- 解析import/export语句
- 合并多个文件
- 处理依赖关系
- 代码分割和优化
3、运行时执行流程
初始化阶段:
jsx
// 1. 引入React库
import React from 'react';
import ReactDOM from 'react-dom/client';
// 2. 创建根节点
const root = ReactDOM.createRoot(document.getElementById('root'));
// 3. 渲染组件
root.render(<App />);
渲染过程:
- 创建虚拟DOM:React将JSX转换为虚拟DOM对象
- Diff算法:比较新旧虚拟DOM的差异
- 更新真实DOM:将变化应用到浏览器的真实DOM上
原理图如下: 
2.2 Fiber架构
2.2.1 什么是Fiber架构?
Fiber是React 16引入的全新协调引擎(Reconciliation Engine),是React核心算法的完全重写。它解决了旧版本React在处理大型应用时的性能瓶颈问题。
React Fiber的解决方案:可中断的异步渲染,它的核心思想是:将不可中断的同步更新,拆解成可中断的异步工作单元,也就是中断和重启。
2.2.2 为什么需要Fiber架构?
旧架构的问题:
js
// 旧版React的递归更新过程
function updateComponent(component) {
// 递归处理子组件
component.children.forEach(child => {
updateComponent(child); // 无法中断
});
// 更新DOM
}
// 问题场景:大量组件更新
function heavyUpdate() {
// 假设这个更新需要50ms
for(let i = 0; i < 1000; i++) {
updateComponent(components[i]); // 同步执行
}
// 50ms > 16.6ms,会导致掉帧
}
主要痛点:
- 更新过程是同步且递归的,无法中断
- 大型组件树更新时会长时间占用主线程
- 导致页面卡顿、动画掉帧、用户输入延迟
为什么浏览器卡顿:
- 浏览器的渲染和js的执行是互斥的:js可以操作dom,如果在渲染的时候操作了dom,不知道以哪个为准。
- 一般浏览器的刷新率为60hz,即1秒钟刷新60次。1000ms / 60hz = 16.6ms ,大概每过16.6ms浏览器会渲染一帧画面,也就是一次eventLoop需要保证在16.6ms内完成。
- 在需要连续渲染的时候,如果js的执行超过16.6ms,导致染间隔大于了16.6ms,就会导致卡顿。
2.2.3 Fiber节点结构
js
// Fiber节点的简化结构
const fiber = {
// 节点类型信息
type: 'div',
key: null,
// 关系指针
return: parentFiber, // 父节点
child: firstChildFiber, // 第一个子节点
sibling: nextSiblingFiber, // 下一个兄弟节点
// 状态和属性
props: {},
state: {},
// 副作用
effectTag: 'UPDATE', // 'PLACEMENT' | 'UPDATE' | 'DELETION'
// 双缓冲
alternate: workInProgressFiber, // 指向另一棵树的对应节点
};
2.2.4 Fiber的工作原理

时间切片(Time Slicing):
jsx
// Fiber将工作分成小单元
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
// 执行一个工作单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// 检查是否需要让出控制权
shouldYield = deadline.timeRemaining() < 1;
}
// 如果还有工作,继续调度
if (nextUnitOfWork) {
requestIdleCallback(workLoop);
}
}
requestIdleCallback(workLoop);
双缓冲机制:
js
// current树:当前显示在屏幕上的
// workInProgress树:正在构建的新树
function commitRoot() {
// 工作完成后,交换两棵树
root.current = workInProgressRoot;
}
Fiber的两个阶段:
js
// 1. 协调阶段 - 可以被打断
function reconciliation() {
// 遍历Fiber树
// 标记需要更新的节点
// 计算副作用
// 这个过程可以暂停和恢复
while (workInProgress && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
// 2. 提交阶段 - 必须同步完成
function commit() {
// 一次性应用所有DOM变更
commitBeforeMutationEffects(); // DOM变更前
commitMutationEffects(); // 执行DOM操作
commitLayoutEffects(); // DOM变更后
}
效果对比:
js
// 旧架构:一次性处理完整棵树
function oldRender() {
// 阻塞主线程 100ms
updateEntireTree(); // 无法中断
}
用户点击 → 开始更新 → [50ms阻塞] → 更新完成 → 响应用户
↑
页面卡顿
// Fiber架构:分片处理
function fiberRender() {
// 每5ms处理一部分
while (hasWork && timeRemaining() > 0) {
processChunk(); // 可以随时暂停
}
// 让出控制权给浏览器处理用户输入、动画等
}
用户点击 → 开始更新 → [5ms工作] → 让出控制权 → [5ms工作] → ...
↑ ↑
可响应用户 可处理动画