前端八股文面经大全:字节暑期前端一面(2026-04-21)·面经深度解析

前言

大家好,我是木斯佳。

相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。

这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。专栏快速地址

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。

面经原文内容

📍面试公司:字节跳动

🕐面试时间:近期

💻面试岗位:暑期前端一面

❓面试问题:

  1. Sdk干什么,你为什么会在公司接触这个业务
  2. 插件是什么个逻辑
  3. sdk的treeshaking前后的大小有没有对比过
  4. treeshaking的逻辑
  5. treeshaking是在哪个阶段进行
  6. treeshaking在cjs有什么限制
  7. monorepo底层逻辑架构是什么样
  8. md里面的ai相关是什么逻辑
  9. md的优化性能优化逻辑
  10. 有没有其他的性能优化
  11. react周期
  12. 虚拟diff对比
  13. 为什么不能写在条件里面,如果一定要写怎么办
  14. git多个commit怎么合并一个
  15. sass, less, tailwind css区别

手撕

  1. bind
  2. 数字转汉字整数

来源:牛客网123456789___

💡 木木有话说(刷前先看)

字节的实习面质量都比较高。这一篇面经,面试官显然对构建优化和工程化细节 非常关注,问到了Tree Shaking的阶段、CJS的限制等底层问题。适合有一定打包工具和工程化经验的同学参考。


📝 字节暑期前端一面·深度解析

🎯 面试整体画像

维度 特征
面试风格 工程化深挖型 + 构建原理型 + 实战手撕型
难度评级 ⭐⭐⭐⭐(四星,Tree Shaking细节、Monorepo架构较深)
考察重心 SDK/插件设计、Tree Shaking原理、Monorepo、React核心、Git、CSS工具
特殊之处 对Tree Shaking的阶段和CJS限制追问很细,考察构建工具理解深度

🔍 逐题深度解析

一、SDK干什么,为什么会在公司接触这个业务

回答思路:SDK(Software Development Kit)是为第三方开发者提供的工具包。

SDK的作用

  • 封装复杂逻辑,提供简洁API
  • 跨项目复用业务能力(如埋点、登录、支付)
  • 版本独立演进,业务方无感知升级

接触原因:公司需要将核心能力(如监控、AI能力)输出给内部其他业务线或外部客户,通过SDK方式接入。


二、插件是什么逻辑

回答思路:插件是一种扩展机制,在不修改核心代码的情况下增加功能。

插件设计模式

  1. 定义接口:规定插件的生命周期(install、init、destroy)
  2. 注册机制:插件向核心注册,核心维护插件列表
  3. 钩子(Hook):核心在特定时机调用插件的方法
javascript 复制代码
// 插件架构示例
class PluginManager {
  constructor() {
    this.plugins = []
  }
  
  use(plugin) {
    this.plugins.push(plugin)
    plugin.install?.(this)
  }
  
  async callHook(hookName, ...args) {
    for (const plugin of this.plugins) {
      await plugin[hookName]?.(...args)
    }
  }
}

三、SDK的Tree Shaking前后大小对比

回答思路:Tree Shaking能移除未使用的代码,显著减小打包体积。

对比维度

  • 未优化:全量打包,包含所有API和依赖
  • 优化后:只打包被引用的代码,体积可减少30%-70%

衡量方式 :使用webpack-bundle-analyzerrollup-plugin-visualizer分析。


四、Tree Shaking的逻辑

回答思路 :Tree Shaking是静态分析过程,标记未使用的代码并在打包时删除。

核心逻辑

  1. ES Module静态结构import/export在编译时确定,无法动态修改
  2. 标记未使用:从入口开始,追踪所有被引用的导出
  3. 删除死代码:未被标记的导出在打包时被移除
javascript 复制代码
// 被标记为未使用的函数
export function used() { console.log('used') }
export function unused() { console.log('unused') }  // 会被移除

// 副作用标记:package.json中的"sideEffects": false

五、Tree Shaking在哪个阶段进行

答案打包阶段(Bundle Time),具体在模块解析和代码生成之间。

流程

  1. 解析阶段:构建依赖图,标记每个导出是否被使用
  2. 优化阶段:删除未使用的导出和模块
  3. 生成阶段:只输出被使用的代码

工具:Webpack、Rollup、Vite(生产环境使用Rollup)都在打包阶段执行。


六、Tree Shaking在CJS有什么限制

回答思路 :CommonJS是动态模块系统,无法静态分析。

限制

  1. 动态导入require(condition ? 'a' : 'b') 无法确定依赖
  2. 导出可变module.exports可以在运行时修改,无法静态分析
  3. 无法确定引用require的返回值可以像普通对象一样被动态访问
javascript 复制代码
// CJS无法Tree Shaking的例子
const utils = require('./utils')
const methodName = Math.random() > 0.5 ? 'foo' : 'bar'
utils[methodName]()  // 无法确定调用了哪个方法

// ESM静态结构,可分析
import { foo, bar } from './utils'  // 明确引用了哪些

Webpack处理 :需要配置optimization.usedExports,但效果有限。


七、Monorepo底层逻辑架构

回答思路 :Monorepo用单一仓库管理多个项目/包,核心是依赖管理和构建优化

架构逻辑

  1. 依赖提升:公共依赖提升到根目录,减少重复安装
  2. 软链接:本地包之间通过软链接引用,无需发布
  3. 增量构建:只构建变更的包及其依赖
  4. 版本管理:统一版本号或独立版本(如Lerna)

工具对比

工具 特点
pnpm workspace 硬链接,节省磁盘空间,严格依赖隔离
Turborepo 智能缓存,增量构建
Nx 依赖图可视化,支持多种框架
Lerna 版本管理成熟,发布流程完善
yaml 复制代码
# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'apps/*'

八、md里面的AI相关是什么逻辑

回答思路:可能指Markdown文件中的AI辅助功能(如智能补全、摘要生成)。

逻辑

  1. 内容解析:解析Markdown AST,提取标题、段落、代码块
  2. AI调用:将内容发送给LLM,生成摘要/标签/翻译
  3. 结果注入:将AI生成的内容写入Frontmatter或特定位置
javascript 复制代码
// 示例:为Markdown生成AI摘要
async function generateSummary(mdContent) {
  const response = await fetch('/api/ai/summary', {
    body: JSON.stringify({ content: mdContent })
  })
  const { summary } = await response.json()
  return summary
}

九、MD的性能优化逻辑

回答思路:Markdown内容渲染和编辑的性能优化。

优化策略

  1. 懒加载:分页加载长文档,按需渲染
  2. 增量解析:只重新解析编辑的部分
  3. 虚拟滚动:文档过长时,只渲染可视区域
  4. 防抖渲染:编辑时延迟渲染,避免频繁重绘
  5. Web Worker解析:将Markdown解析移到Worker线程
javascript 复制代码
// 增量解析示例
let timer
editor.on('input', () => {
  clearTimeout(timer)
  timer = setTimeout(() => {
    // 只重新解析变化的部分
    incrementalParse(changedRange)
  }, 300)
})

十、有没有其他的性能优化

回答思路:列举Web常见性能优化手段。

分类

  • 加载优化:代码分割、图片懒加载、预加载关键资源
  • 渲染优化:虚拟列表、transform动画、减少重排
  • 网络优化:HTTP/2、缓存策略(强缓存+协商缓存)、CDN
  • 运行时优化:防抖节流、Web Worker计算、内存管理

十一、React生命周期

回答思路:类组件生命周期 vs 函数组件Hook。

类组件

  • 挂载:constructorrendercomponentDidMount
  • 更新:shouldComponentUpdaterendercomponentDidUpdate
  • 卸载:componentWillUnmount

函数组件(Hook替代)

  • useEffect(() => {}, [])componentDidMount
  • useEffect(() => () => {})componentWillUnmount
  • useEffect(() => {}, [deps])componentDidUpdate

十二、虚拟Diff对比

回答思路:虚拟DOM的diff算法是O(n)复杂度的启发式算法。

核心规则

  1. 同层比较:不跨层级比较
  2. 类型不同:直接销毁重建
  3. 类型相同:保留DOM,更新属性
  4. 子节点列表:使用key优化移动、插入、删除
javascript 复制代码
// 简化diff逻辑
function diff(oldNode, newNode) {
  if (oldNode.type !== newNode.type) {
    return { type: 'REPLACE', newNode }
  }
  if (oldNode.type === 'text') {
    if (oldNode.content !== newNode.content) {
      return { type: 'TEXT', content: newNode.content }
    }
    return null
  }
  // 比较属性...
}

十三、为什么不能写在条件里面,如果一定要写怎么办

回答思路:React Hooks必须在组件顶层调用,不能写在条件/循环中。

原因

  • React依赖调用顺序来关联Hook与状态
  • 条件语句会改变调用顺序,导致状态错乱

如果一定要条件执行

  • 将条件逻辑移到Hook内部
  • 使用自定义Hook封装条件逻辑
javascript 复制代码
// ❌ 错误
if (condition) {
  useEffect(() => { ... }, [])  // 违反规则
}

// ✅ 正确:条件在Hook内部
useEffect(() => {
  if (condition) {
    // 执行逻辑
  }
}, [condition])

十四、Git多个commit怎么合并一个

回答思路 :使用git rebase -i进行交互式变基。

步骤

bash 复制代码
# 合并最近3个commit
git rebase -i HEAD~3

# 在编辑器中,将想要合并的commit前面的pick改为squash(或s)
# pick abc123 commit 1
# squash def456 commit 2
# squash ghi789 commit 3

# 保存退出,编辑合并后的commit message

其他方法

  • git reset --soft HEAD~3 + git commit -m "new message"(更简单,但会丢失中间commit信息)
  • git merge --squash(合并分支时使用)

十五、Sass、Less、Tailwind CSS区别

维度 Sass/SCSS Less Tailwind CSS
类型 CSS预处理器 CSS预处理器 CSS框架(原子类)
变量 $ @ 配置文件
嵌套
混入(Mixin) @mixin / @include 函数
继承 @extend @apply
原子类 需自定义 需自定义 内置(flexp-4
学习曲线 中等 中等
最终体积 按需打包 按需打包 需PurgeCSS优化

选择建议

  • Sass:功能最强大,适合复杂项目
  • Less:简单易学,Node.js生态
  • Tailwind:快速开发,避免命名困扰,适合组件化项目

手撕一:实现bind

题目 :手写Function.prototype.bind

javascript 复制代码
Function.prototype.myBind = function(context, ...args) {
  if (typeof this !== 'function') {
    throw new TypeError('Bind must be called on a function')
  }
  
  const fn = this
  
  return function bound(...newArgs) {
    // 判断是否作为构造函数调用
    if (this instanceof bound) {
      // 构造函数调用:原函数作为构造函数,忽略绑定的context
      return new fn(...args, ...newArgs)
    }
    // 普通调用:绑定this
    return fn.apply(context, [...args, ...newArgs])
  }
}

// 使用示例
const obj = { name: 'Tom' }
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`
}
const boundGreet = greet.myBind(obj, 'Hello')
console.log(boundGreet('!'))  // Hello, Tom!

手撕二:数字转汉字整数

题目:将数字(0-99999)转换为中文汉字。

javascript 复制代码
function numberToChinese(num) {
  if (num === 0) return '零'
  
  const digits = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']
  const units = ['', '十', '百', '千']
  const bigUnits = ['', '万']
  
  function convertSection(n) {
    if (n === 0) return ''
    let result = ''
    let zeroFlag = false
    
    for (let i = 3; i >= 0; i--) {
      const divisor = Math.pow(10, i)
      const digit = Math.floor(n / divisor) % 10
      
      if (digit === 0) {
        zeroFlag = true
      } else {
        if (zeroFlag) {
          result += '零'
          zeroFlag = false
        }
        result += digits[digit] + units[i]
      }
    }
    return result
  }
  
  let result = ''
  let zeroSection = false
  
  for (let i = 1; i >= 0; i--) {
    const divisor = Math.pow(10000, i)
    const section = Math.floor(num / divisor) % 10000
    
    if (section === 0) {
      zeroSection = true
    } else {
      if (zeroSection && result !== '') {
        result += '零'
        zeroSection = false
      }
      result += convertSection(section) + bigUnits[i]
    }
  }
  
  // 处理边界情况:十
  if (result.startsWith('一十')) result = result.slice(1)
  
  return result
}

// 测试
console.log(numberToChinese(12345))  // 一万二千三百四十五
console.log(numberToChinese(10001))  // 一万零一
console.log(numberToChinese(10))     // 十

📚 知识点速查表

知识点 核心要点
SDK 封装能力、跨项目复用、独立演进
插件 接口定义、注册机制、钩子调用
Tree Shaking 静态分析ESM、打包阶段、移除未使用代码
CJS限制 动态导入/导出,无法静态分析
Monorepo 依赖提升、软链接、增量构建
虚拟Diff 同层比较、key优化、类型决定策略
Hook规则 顶层调用,原因:依赖调用顺序
Git合并commit git rebase -i + squash
CSS工具 Sass/Less预处理器,Tailwind原子类
bind实现 绑定this,注意构造函数调用
数字转汉字 分段处理、零的处理、万级单位

📌 最后一句:

字节这场一面,面试官明显是工程化背景深厚 的技术专家。从SDK设计、Tree Shaking底层原理、Monorepo架构,到CJS限制、Hook规则,每一题都在考察你是否理解工具背后的原理 而非表面用法。能答好这套题,说明你不仅会用Webpack/Vite,更知道它们为什么这样设计、有什么局限。这种深度,正是字节面试的典型风格。

相关推荐
我叫黑大帅2 小时前
其实跨域问题是后端来解决的? CORS
后端·面试·go
Jolyne_2 小时前
前端从0开始的LangChain学习(一)
前端·langchain
掘金一周2 小时前
掘友们,一人说一个你买过夯到爆的东西 | 沸点周刊 4.23
前端·人工智能·后端
Developer_Niuge2 小时前
告别翻不动的 1000+ 书签:开源 Chrome / Edge 浏览器书签管理插件 Smart Bookmark 0.2 发布
前端·后端
WebInfra2 小时前
Rsbuild 2.0 发布:即将支持 TanStack Start
前端·javascript·程序员
用户52709648744902 小时前
前端性能指标速查手册
前端
淹死在鱼塘的程序猿2 小时前
🚀 告别"一次性聊天":揭秘让 AI 智能体越用越聪明的秘密武器 —— Skills
前端·人工智能·agent
掘金安东尼2 小时前
OpenMUSE 全面详解:非扩散Transformer文生图开源基座(对标GPT Image 2)
前端·javascript·面试
下次再写2 小时前
Java互联网大厂面试技术问答实战:涵盖Java SE、Spring Boot、微服务及多场景应用
java·数据库·缓存·面试·springboot·microservices·技术问答