跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScriptNode.js大前端算法

字节跳动前端一面深度解析:React 原理与浏览器渲染

综述由AI生成面试问题 📍面试公司:字节跳动 🕐面试时间:近期 💻面试岗位:前端一面 ⏱️面试时长:未提及 📝面试体验:难度 Plus Ultra 版,苦战,加粗的是没答上来的 ❓面试问题: Reconciler 如何遍历 fiber 树(先序遍历) 为什么要这么设计 DOM 树和 fiber 树的区别 diff 算法是怎么比较新旧两个树的 浏览器从拿到渲染树以后都经过了哪些阶段(布局→分层→分块→光栅…

林间仙子发布于 2026/4/6更新于 2026/5/2810K 浏览
字节跳动前端一面深度解析:React 原理与浏览器渲染

面试问题

📍面试公司:字节跳动
🕐面试时间:近期
💻面试岗位:前端一面
⏱️面试时长:未提及
📝面试体验:难度 Plus Ultra 版,苦战,加粗的是没答上来的

❓面试问题:

  1. Reconciler 如何遍历 fiber 树(先序遍历)
  2. 为什么要这么设计
  3. DOM 树和 fiber 树的区别
  4. diff 算法是怎么比较新旧两个树的
  5. 浏览器从拿到渲染树以后都经过了哪些阶段(布局→分层→分块→光栅化→直接显示(其实是合成))
  6. 为什么光栅化要由 GPU 去做
  7. 为什么会这样呢
  8. Webpack 和 Vite 有什么区别
  9. Vite 打包用的什么
  10. ESM 和 CJS 区别(提到同步导入和异步导入)
  11. 微任务队列和宏任务队列都是什么
  12. 任务循环在浏览器和 Node 有什么区别
  13. Message channel 是什么
  14. 为什么 React 用了 Message channel 调度没用 setTimeout
  15. 听说过 React 时间分片吗
  16. 说一下 JavaScript 是不是单线程的语言
  17. 用过哪些设计模式
  18. 手撕:同时允许 2 个任务执行的异步调度器
  19. 手撕:两个有序数组合并成一个有序数组

字节跳动前端一面深度解析

面试整体画像
维度特征
面试风格源码级深挖型 + 底层原理型 + 追根究底型
难度评级⭐⭐⭐⭐(四星半,React 原理 + 浏览器底层 + 工程化深度)
考察重心React fiber 架构、浏览器渲染流水线、构建工具原理、事件循环机制、设计模式
特殊之处问题层层递进,连续追问'为什么这样设计',考察真正的理解深度而非背诵
逐题深度解析
一、Reconciler 如何遍历 fiber 树(先序遍历)

回答思路:这是 React fiber 架构的核心。Reconciler(协调器)负责找出组件树的变化,它采用深度优先遍历(DFS),具体是先序遍历(pre-order)。

遍历过程:

  1. 从根 fiber 开始,先处理当前节点
  2. 如果有 child,进入 child
  3. child 处理完后,如果有 sibling,进入 sibling
  4. 重复直到完成所有节点
// 伪代码示意
function workLoop(unitOfWork) {
  while (unitOfWork !== null) {
    // 处理当前节点(beginWork)
    unitOfWork = beginWork(unitOfWork);
    // 如果有 child,继续向下
    if (unitOfWork !== null && unitOfWork.child !== null) {
      unitOfWork = unitOfWork.child;
    } else {
      // 没有 child,向上返回
      while (unitOfWork !== null) {
        // 完成当前节点(completeWork)
        completeWork(unitOfWork);
        // 有 sibling,转到 sibling
        if (unitOfWork.sibling !== null) {
          unitOfWork = unitOfWork.sibling;
          break;
        }
        // 否则返回父节点
        unitOfWork = unitOfWork.return;
      }
    }
  }
}
二、为什么要这么设计

回答思路:这是追问'为什么是 DFS,而不是 BFS'。考察对 React 设计意图的理解。

核心原因:

  1. 可中断性:React 需要实现'时间分片'(time slicing),DFS 可以随时暂停和恢复,因为每个节点有明确的'return'指针指向父节点。BFS 需要维护整个层级队列,恢复成本高。
  2. 优先级调度:DFS 便于按优先级处理节点,可以优先处理用户交互相关的分支(如输入框所在的子树)。
  3. 生命周期对应:组件挂载/更新的生命周期(componentDidMount、useEffect)需要在子树完全处理完后执行,DFS 的'递'阶段(beginWork)和'归'阶段(completeWork)天然匹配这一需求。
  4. 内存效率:DFS 只需维护当前路径的节点引用,BFS 需要维护整个队列。
三、DOM 树和 fiber 树的区别

回答思路:从目的、结构、可变性等方面对比。

维度DOM 树Fiber 树
目的页面渲染的结构表示React 内部的工作单元,用于调度渲染
节点关系parent、children(单向)child、sibling、return(双向链表)
可变性不可变(更新会创建新节点)可复用(fiber 节点可以保留、更新)
生命周期与页面渲染绑定独立于渲染,可暂停/恢复
内容存储样式、属性等渲染信息存储组件类型、state、props、副作用列表

核心:fiber 树是 React 自己的数据结构,它的设计是为了增量渲染——把渲染任务拆分成多个小任务,分散到多个帧中执行。

四、diff 算法是怎么比较新旧两个树的

回答思路:React 的 diff 算法基于三个假设:

  1. 不同类型的元素产生不同的树
  2. 开发者可以通过 key prop 暗示哪些子元素是稳定的
  3. 只进行同层比较,不跨层比较

比较过程:

  1. 节点类型不同:直接销毁旧子树,新建新子树
  2. 节点类型相同(DOM 元素):保留 DOM 节点,更新变化的属性
  3. 节点类型相同(组件):组件实例不变,更新 props,触发生命周期
  4. 子节点列表比较:使用 key 进行优化,通过移动、插入、删除操作最小化变更
// 子节点比较核心逻辑(简化)
function reconcileChildren(prevChildren, nextChildren) {
  // 使用 key 建立映射
  const prevMap = new Map();
  prevChildren.forEach(child => prevMap.set(child.key, child));
  const newChildren = [];
  let lastIndex = 0;
  nextChildren.forEach(nextChild => {
    const prevChild = prevMap.get(nextChild.key);
    if (prevChild) {
      if (prevChild.index < lastIndex) {
        // 需要移动
        markMove(prevChild);
      } else {
        lastIndex = prevChild.index;
      }
      // 更新节点
      updateNode(prevChild, nextChild);
      newChildren.push(prevChild);
    } else {
      // 新增节点
      const newFiber = createFiber(nextChild);
      newChildren.push(newFiber);
    }
  });
  return newChildren;
}
五、浏览器渲染阶段(从渲染树到显示)

回答思路:完整流程如下:

布局(Layout)→ 分层(Layer)→ 分块(Tiling)→ 光栅化(Rasterization)→ 合成(Composite)

各阶段说明:

  • 布局:计算每个元素的位置和尺寸,生成 Layout Tree
  • 分层:根据层叠上下文、transform、will-change 等属性,将页面拆分成多个图层(Layer)
  • 分块:将每个图层分成若干图块(Tile),通常是 256x256 或 512x512 大小
  • 光栅化:将图块转换成位图(像素信息),GPU 负责执行
  • 合成:将各个图层的位图按照顺序合成为最终显示的图像,由 GPU 的合成器(Compositor)完成

注意:最后一步是合成,不是直接显示。

六、为什么光栅化要由 GPU 去做

回答思路:从 GPU 的架构优势出发。

原因:

  1. 并行计算能力:光栅化是'将向量图形转换为像素'的过程,每个像素可以独立计算。GPU 有数千个核心,天然适合这种大规模并行任务。
  2. 硬件优化:GPU 专为图形处理设计,有专门的纹理映射、抗锯齿、透明度混合等硬件单元。
  3. 效率:CPU 做光栅化需要逐像素循环,速度慢;GPU 可以同时处理大量图块。
  4. 帧率保障:60fps 需要 16.6ms 内完成一帧,GPU 能保证合成器快速合成。
七、为什么会这样呢(GPU 架构)

回答思路:这是上一题的'追问到底',考察对 GPU 原理的理解。

GPU 的核心特点:

  • SIMD(单指令多数据流):一条指令控制多个处理单元同时执行相同操作,适合像素处理
  • 高吞吐量:GPU 有数千个计算核心,虽然单核比 CPU 慢,但总吞吐量是 CPU 的数十倍
  • 内存带宽高:GPU 有专用的显存(VRAM),带宽远超系统内存
八、Webpack 和 Vite 的区别

回答思路:从开发体验、构建方式、生产打包等方面对比。

维度WebpackVite
开发环境打包所有模块,启动慢利用 ESM,直接启动,秒级
热更新重新打包相关模块,慢利用 ESM 的 HMR,只更新变更的模块,快
生产打包统一打包成 bundle使用 Rollup 预打包,优化较好
配置复杂度高,需要大量配置低,零配置开箱即用
生态成熟,插件丰富快速追赶,生态渐全

核心区别:Vite 利用浏览器原生 ESM 支持,开发环境不打包,启动和热更新更快;Webpack 需要在开发环境也打包所有模块。

九、Vite 打包用的什么

回答思路:Vite 开发环境用ESM(原生模块),生产打包用的是Rollup。因为生产环境需要更精细的优化(tree-shaking、代码分割、兼容性处理),Rollup 在这些方面做得更好。

十、ESM 和 CJS 区别

回答思路:核心区别是同步导入和异步导入。

维度CJS(CommonJS)ESM(ES Module)
加载方式同步(require)异步(import)
执行时机运行时执行编译时解析
导出module.exportsexport default / export
静态分析不支持支持(tree-shaking 依赖)
浏览器支持需打包原生支持
循环依赖有坑(拿到的是部分导出)更好处理(实时绑定)

关键点:CJS 的 require 是同步的,在服务器端(Node.js)没问题;ESM 的 import 是异步的,适合浏览器环境。

十一、微任务队列和宏任务队列

回答思路:微任务队列优先级高于宏任务队列,在当前宏任务执行完后、下一个宏任务开始前清空。

十二、事件循环在浏览器和 Node 的区别

回答思路:

维度浏览器Node
宏任务setTimeout、setInterval、I/O、UI 渲染setTimeout、setInterval、setImmediate、I/O
微任务Promise.then、MutationObserverPromise.then、process.nextTick
阶段简单(宏任务→微任务→渲染)复杂(timers→pending→idle→poll→check→close)
process.nextTick无优先级高于 Promise,在每阶段结束后立即执行

Node 事件循环阶段:

  1. timers:执行 setTimeout/setInterval 的回调
  2. pending:执行上一轮遗留的 I/O 回调
  3. idle/prepare:内部使用
  4. poll:获取新的 I/O 事件,执行相关回调
  5. check:执行 setImmediate 回调
  6. close:执行 close 事件回调
十三、Message channel 是什么

回答思路:MessageChannel 是浏览器提供的通信 API,用于在不同执行上下文(如主线程和 Web Worker)之间传递消息,也可以在同一线程的不同任务之间传递。

const channel = new MessageChannel();
const port1 = channel.port1;
const port2 = channel.port2;
port1.onmessage = (e) => console.log(e.data);
port2.postMessage('hello'); // port1 收到消息

在 React 中的作用:React 用它来模拟 requestIdleCallback,实现时间分片调度。因为 setTimeout 有最小 4ms 延迟(嵌套时),不适合高精度调度;MessageChannel 可以做到 0 延迟的宏任务,且不阻塞渲染。

十四、为什么 React 用 MessageChannel 调度,没用 setTimeout

回答思路:

核心原因:

  1. setTimeout 有延迟:嵌套的 setTimeout 最小延迟是 4ms,即使写 setTimeout(fn, 0),实际也会等待至少 4ms。这会让 React 的时间分片颗粒度过粗。
  2. MessageChannel 是 0 延迟:通过 MessageChannel 派生的宏任务,可以在下一帧立即执行,没有最小延迟。
  3. 优先级调度:React 需要区分高优先级(用户输入)和低优先级(数据更新),MessageChannel 可以配合 requestAnimationFrame 实现精确的优先级调度。
  4. 与渲染帧对齐:React 需要在每帧结束前执行低优先级任务,避免掉帧。MessageChannel 能更好地控制时机。
// React 调度器简化逻辑
let scheduledCallback = null;
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = () => {
  if (scheduledCallback) {
    const callback = scheduledCallback;
    scheduledCallback = null;
    callback();
  }
};
function scheduleCallback(callback) {
  scheduledCallback = callback;
  port.postMessage(null); // 触发宏任务
}
十五、听说过 React 时间分片吗

回答思路:

时间分片(Time Slicing):React 将渲染任务拆分成多个小任务(每个 fiber 节点是一个任务),每个任务执行一段时间(默认 5ms),然后检查是否需要让出主线程(如是否有用户输入等待处理)。如果需要,就暂停,把控制权交还给浏览器,等下一帧再继续。这保证了页面在高频更新时(如长列表渲染)不会卡死。

十六、JavaScript 是不是单线程的语言

回答思路:

  • JavaScript 语言本身是单线程的,它有且只有一个调用栈,一次只能执行一段代码。
  • 浏览器环境是多线程的:主线程(JS 引擎 + 渲染)、Web Worker 线程(可运行 JS)、网络线程、定时器线程、GPU 线程等。
  • JS 引擎的单线程指执行 JS 代码的线程只有一个,但浏览器通过事件循环和异步 API(Web Worker)提供了并发能力。
十七、用过哪些设计模式

回答思路:常见设计模式:

模式使用场景
单例全局状态管理(Vuex/Pinia)
观察者事件总线、响应式系统
工厂创建不同组件(如弹窗类型)
策略表单校验规则
装饰器HOC(高阶组件)
发布订阅跨组件通信

回答示例:'我在项目中使用过策略模式来处理表单校验。不同字段的校验规则不同(手机号、邮箱、非空),我把校验函数抽象成策略对象,根据字段类型动态选择。这样新增校验规则时不需要修改原有代码,符合开闭原则。'

十八、手撕:同时允许 2 个任务执行的异步调度器

题目:实现一个异步调度器,最多同时执行 2 个任务,任务完成后自动执行队列中的下一个。

class Scheduler {
  constructor(limit = 2) {
    this.limit = limit;
    this.running = 0;
    this.queue = [];
  }

  add(promiseFactory) {
    return new Promise((resolve, reject) => {
      this.queue.push(() => {
        promiseFactory().then(resolve, reject).finally(() => {
          this.running--;
          this.next();
        });
      });
      this.next();
    });
  }

  next() {
    if (this.running < this.limit && this.queue.length) {
      const task = this.queue.shift();
      this.running++;
      task();
    }
  }
}

// 使用示例
const scheduler = new Scheduler(2);
const timeout = (time, order) => new Promise(resolve => {
  setTimeout(() => {
    console.log(order);
    resolve();
  }, time);
});

scheduler.add(() => timeout(1000, '1'));
scheduler.add(() => timeout(500, '2'));
scheduler.add(() => timeout(300, '3'));
scheduler.add(() => timeout(400, '4'));
// 输出顺序:2 3 1 4
十九、手撕:两个有序数组合并成一个有序数组
function mergeSortedArrays(arr1, arr2) {
  const result = [];
  let i = 0, j = 0;
  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] < arr2[j]) {
      result.push(arr1[i]);
      i++;
    } else {
      result.push(arr2[j]);
      j++;
    }
  }
  // 处理剩余元素
  while (i < arr1.length) result.push(arr1[i++]);
  while (j < arr2.length) result.push(arr2[j++]);
  return result;
}
知识点速查表
知识点核心要点
fiber 树遍历深度优先、先序遍历,支持可中断恢复
fiber 设计原因时间分片、优先级调度、生命周期匹配
DOM 树 vs fiber 树目的、节点关系、可变性、内容差异
diff 算法同层比较、key 优化、类型决定策略
渲染流水线布局→分层→分块→光栅化→合成
GPU 光栅化并行计算、硬件优化、高吞吐量
Webpack vs Vite开发体验、构建方式、生产打包、配置复杂度
ESM vs CJS同步/异步、静态/运行时、浏览器支持
事件循环(Node)多阶段、process.nextTick 优先级高
MessageChannel跨线程通信、0 延迟宏任务、React 调度器
时间分片5ms 切片,优先响应用户交互
异步调度器并发控制、任务队列、Promise 返回

目录

  1. 面试问题
  2. 字节跳动前端一面深度解析
  3. 面试整体画像
  4. 逐题深度解析
  5. 一、Reconciler 如何遍历 fiber 树(先序遍历)
  6. 二、为什么要这么设计
  7. 三、DOM 树和 fiber 树的区别
  8. 四、diff 算法是怎么比较新旧两个树的
  9. 五、浏览器渲染阶段(从渲染树到显示)
  10. 六、为什么光栅化要由 GPU 去做
  11. 七、为什么会这样呢(GPU 架构)
  12. 八、Webpack 和 Vite 的区别
  13. 九、Vite 打包用的什么
  14. 十、ESM 和 CJS 区别
  15. 十一、微任务队列和宏任务队列
  16. 十二、事件循环在浏览器和 Node 的区别
  17. 十三、Message channel 是什么
  18. 十四、为什么 React 用 MessageChannel 调度,没用 setTimeout
  19. 十五、听说过 React 时间分片吗
  20. 十六、JavaScript 是不是单线程的语言
  21. 十七、用过哪些设计模式
  22. 十八、手撕:同时允许 2 个任务执行的异步调度器
  23. 十九、手撕:两个有序数组合并成一个有序数组
  24. 知识点速查表
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • OpenClaw 构建飞书 AI 办公机器人:本地 Ollama 接入与 Skills 自动化
  • DooTask 轻量级项目管理与 AI 协同功能解析
  • MySQL 数据类型核心指南:选型、实战与避坑
  • Seq2Seq 模型实战:ScheduledEmbeddingTrainingHelper 原理与使用
  • Cursor 集成 MCP 服务实战指南:从配置到自动化任务执行
  • pyenv-win Python 多版本管理实战与效率优化方案
  • JavaScript 调试技巧与实用工具指南
  • 基于 GLM4.7 的 Claude Code GitHub 代码自动审查
  • AI 对话页流式处理架构:Web Streams 与 Fetch API 实践
  • AIGC 产品经理工作职责与职位要求解析
  • Spring Boot 全局异常处理与日志监控实战
  • Linux 命名管道(FIFO)通信:原理与跨进程实战
  • Java 大数据量 Excel 导入导出实现方案
  • Rust 异步微服务架构最佳实践与反模式规避
  • Mac mini M4 部署 OpenClaw + Ollama 本地大模型接入飞书机器人
  • Z-Image-Turbo 孙珍妮模型使用指南
  • Python 3.8+ 海象运算符详解
  • Java 数据类型、运算符与方法核心总结
  • 后端开发必备:HTML 基础语法与实战入门
  • 前端数据库 IndexedDB 详解:构建离线 Web 应用

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online