字节一面面经(飞书项目)

6 阅读8分钟

前言

前面经过滴滴的两轮面试过后,又有大厂面试,这次是字节的一面,接下来是字节的面经


1. 为什么选择 react + tailwind + shadcn + zustand 技术栈?

  • React:成熟生态、组件化、Hooks 范式统一状态逻辑;
  • Tailwind CSS:原子化 CSS,开发效率高,避免样式冲突,适合快速迭代产品;
  • shadcn/ui:非 npm 包,而是可复制的 UI 组件库(基于 Radix UI + Tailwind),完全可控、可定制、无黑盒,适配设计系统;
  • Zustand:轻量级状态管理(≈2KB),API 简洁(create + useStore),支持中间件、异步、持久化,比 Redux 更符合现代 React 思维;
  • 组合优势:零配置、高性能、易维护、强扩展性,特别适合中后台/工具类产品。

⚠️ 注意:shadcn 不是库,是“模板集合”,需手动复制到项目中;Zustand 无 Provider 嵌套,避免 Context 深层传递问题。


2. 文章列表优化(性能角度)

核心目标:降低首屏时间、提升滚动流畅度(60fps)
常用手段:

  • 虚拟滚动(Virtual List) :仅渲染可视区域元素(见问题3详解);
  • 图片懒加载IntersectionObserver + loading="lazy"
  • 骨架屏(Skeleton) :占位提升感知性能;
  • 分页/无限滚动:避免一次性渲染大量 DOM;
  • CSS 优化:避免 margin/padding 触发重排;使用 transform 实现动画(合成层);
  • React.memo / useMemo:防止无关更新导致列表重渲染;
  • 服务端渲染(SSR)或静态生成(SSG) :首屏 HTML 直出,减少白屏。

📊 实测效果:500+ 条目从 1.8s 首屏 → 0.4s;FPS 从 20 → 60。


3. 虚拟列表的实现原理

核心思想:用“高度占位”替代真实 DOM,只渲染可见项。

关键步骤:

  1. 计算总高度totalHeight = itemHeight * itemCount(固定高)或累加动态高度数组;

  2. 确定可视区范围

    • scrollTop:当前滚动偏移;
    • visibleCount = Math.ceil(containerHeight / itemHeight)
    • startIndex = Math.floor(scrollTop / itemHeight)
    • endIndex = startIndex + visibleCount + buffer(缓冲区防白屏);
  3. 渲染

    • 占位 div 高度 = totalHeight
    • 渲染 [startIndex, endIndex] 内的真实子项;
    • 子项 top = startIndex * itemHeight 定位;
  4. 滚动监听onScroll 更新 scrollTop → 触发 re-render;

  5. 边界处理startIndex = Math.max(0, startIndex)endIndex = Math.min(itemCount, endIndex)

优化点:

  • 动态高度:用 Map 缓存已知高度,二分查找起始索引;
  • 防抖滚动事件(但需注意:不能过度防抖,否则快速滚动卡顿);
  • 使用 requestAnimationFrame 同步渲染,避免 layout thrashing。

🧩 开源参考:react-windowvirtuosotiny-virtual-list


4. 文章列表倾向用哪种图片形式?

取决于内容类型 & 交互需求

场景推荐格式原因
文章封面图(大图)WebP体积小(比 JPG 小 25–35%),支持有损/无损,现代浏览器全覆盖
Logo / 图标 / 透明背景PNGSVGPNG 支持 Alpha;SVG 矢量缩放无损,适合简单图形
高清截图 / 设计稿AVIF(可选)压缩率更高(比 WebP 小 20%),但兼容性稍差(Chrome 85+)
兼容老旧环境JPG + PNG 回退保底方案

✅ 最佳实践:

<picture>
  <source srcset="img.webp" type="image/webp">
  <img src="img.jpg" alt="...">
</picture>

5. 常见图片格式及特点

格式压缩透明动画适用场景兼容性
JPG/JPEG有损照片、Banner★★★★★
PNG无损✅(Alpha)图标、截图、带透明图★★★★★
GIF无损✅(1bit)简单动画(<256色)★★★★★
WebP有损/无损通用替代 JPG/PNG★★★★☆(IE 不支持)
AVIF有损/无损高质量压缩需求★★★☆☆(较新)
SVG矢量✅(SMIL/CSS)图标、图表、响应式图形★★★★☆

💡 WebP 是当前最优解;SVG 用于可缩放矢量图;避免 GIF 做复杂动画(用 MP4 + <video> 替代)。


6. JPG 和 PNG 的区别

维度JPGPNG
压缩方式有损(DCT 变换 + 量化)无损(DEFLATE 压缩)
透明通道❌ 不支持✅ 支持 Alpha 通道
色彩深度24-bit(RGB)24/32-bit(RGBA)
文件大小小(适合连续色调图像)大(适合边缘锐利图像)
适用对象照片、自然场景图标、文字截图、带透明背景图
可编辑性多次保存失真累积无损保存,可反复编辑

⚠️ 误用后果:用 JPG 保存带文字的截图 → 锯齿/模糊;用 PNG 存高清照片 → 体积翻倍。


7. POST 和 GET 的适用场景与区别

特性GETPOST
语义获取资源创建/修改资源
参数位置URL Query StringRequest Body
幂等性✅ 是(多次请求结果相同)❌ 否(如创建订单会重复)
缓存✅ 可被浏览器/CDN 缓存❌ 默认不缓存
安全性❌ URL 可能被记录(日志/历史)✅ 相对安全
数据长度受限(URL 长度 ≈ 2KB~8KB)无限制(服务端决定)
编码application/x-www-form-urlencodedapplication/json, multipart/form-data

✅ 正确用法:

  • 获取数据 → GET;
  • 提交表单/上传文件/修改状态 → POST;
  • 删除资源 → DELETE;
  • 上传文件必须用 POST + multipart/form-data

8. 图片/文件如何上传?

流程:

  1. 获取文件

    const file = input.files[0];
    
  2. 构造 FormData

    const fd = new FormData();
    fd.append('file', file);
    fd.append('name', file.name);
    // 可附加元数据
    
  3. 发送请求

    fetch('/upload', {
      method: 'POST',
      body: fd,
      // ⚠️ 不要设 headers!浏览器自动设为 multipart/form-data
    });
    
  4. 进度监控(高级)

    • 使用 XMLHttpRequest(支持 upload.onprogress);
    • ReadableStream + fetch(实验性,需 polyfill);
  5. 分片上传(大文件)

    • 切片 file.slice(start, end)
    • 并发上传 + 记录已传块;
    • 服务端合并。

🔒 安全:前端校验文件类型/大小;服务端二次校验(MIME、病毒扫描)。


9. 项目性能优化方向

分层优化:

层级手段
网络层HTTP/2、资源压缩(Gzip/Brotli)、CDN、预加载(<link rel="preload">
渲染层虚拟滚动、懒加载、骨架屏、减少重排重绘(用 transform/opacity
JS 层代码分割(React.lazy)、Tree Shaking、避免闭包内存泄漏、Web Worker 处理计算
构建层Vite(快构建)、Rollup/Terser 压缩、Image Optimization(自动转 WebP)
运行时使用 useMemo/useCallback 防止无效渲染;React.memo 包裹列表项

📈 关键指标:FCP(首次内容绘制)< 1s,LCP < 2.5s,INP < 200ms。


10. 项目是否有移动端优化?

✅ 有,主要策略:

  • 响应式布局:Flex/Grid + Media Query;

  • 触控适配

    • touchstart/touchmove 替代 mousedown/mousemove
    • preventDefault() 阻止滚动冲突;
    • -webkit-overflow-scrolling: touch 提升滚动流畅度;
  • ** viewport 适配**:

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    
  • 字体/间距调整:移动端字号 ≥ 14px,点击区域 ≥ 44×44px;

  • 离线能力:Service Worker + Cache API 实现 PWA;

  • 性能降级:低端机禁用复杂动画、减少 DOM 节点数。

📱 检测方案:navigator.userAgentwindow.innerWidth + matchMedia


11. PC 导航在顶部一行,移动端在右上角?

纯 CSS 实现(推荐)

.navbar {
  display: flex;
  justify-content: space-between; /* PC:左右分布 */
  align-items: center;
}

.nav-left { order: 1; }
.nav-right { 
  order: 2; 
  margin-left: auto; /* PC:靠右 */
}

@media (max-width: 768px) {
  .navbar { 
    flex-direction: column; 
    align-items: stretch; 
  }
  .nav-left { order: 1; }
  .nav-right { 
    order: 2; 
    align-self: flex-end; /* 移动端:右上角 */
    margin-top: 12px;
  }
}

✅ 优势:零 JS、高性能、语义清晰。


12. 拖拽功能实现,需要哪些 API 或能力?

必备能力:

  • 事件模型

    • mousedown / touchstart → 启动拖拽
    • mousemove / touchmove → 更新位置
    • mouseup / touchend → 结束拖拽
  • 坐标计算

    • clientX/Y(视口坐标)
    • pageX/Y(文档坐标)
    • offsetX/Y(相对于目标元素)
  • 阻止默认行为

    e.preventDefault(); // 防止文本选中/滚动
    
  • 平滑动画

    • requestAnimationFrame 同步渲染;
    • 使用 transform: translate(x, y) 避免重排;
  • 边界限制

    • 计算容器 getBoundingClientRect()
    • 限制 x ∈ [0, containerWidth - elementWidth]
  • 高级需求

    • 拖影:setDragImage()(H5 DnD)或自定义 ::after
    • 跨容器:需全局状态管理(Context/Redux);
    • 磁吸/对齐:计算最近网格点。

📦 生产推荐:@dnd-kit/core(TypeScript 友好,支持触摸、键盘、组合拖拽)。


13. React Router 实现原理?依赖哪些浏览器能力?

核心架构:

  • 底层依赖history 库(封装浏览器 History API);

  • 路由模式

    • Hash 模式#path,依赖 hashchange 事件;
    • History 模式(主流):pushState / replaceState + popstate 事件;
  • 关键流程

    1. <Link to="/x"> → 调用 history.push('/x')
    2. history 执行 window.history.pushState(null, '', '/x')
    3. 浏览器触发 popstate 事件;
    4. history.listen(callback) 捕获事件 → 更新 location
    5. Router 组件接收新 location → 触发 re-render;
    6. Route 组件用 path-to-regexp 匹配路径 → 决定是否渲染;
  • 嵌套路由:通过 children + useRoutes 构建树形匹配;

  • 导航跳转navigate()history.push() → 重新匹配。

浏览器核心能力:

  • window.history(栈管理);
  • popstate / hashchange 事件;
  • URLSearchParams(解析 query);
  • Location 对象(pathname, search, hash)。

💡 注意:pushState 不触发页面刷新,是 SPA 路由基石


14. K 个一组翻转链表(空间 O(1))

算法思路:

  • 递归分治:先检查前 K 个节点是否存在 → 翻转前 K 个 → 递归处理剩余部分;
  • 翻转用三指针法(prev/curr/next);
  • 连接:原头节点变为尾节点,指向后续翻转结果。

代码(TypeScript):

function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
  // 1. 检查是否足够 k 个节点
  let curr = head;
  for (let i = 0; i < k; i++) {
    if (!curr) return head; // 不足 k 个,直接返回
    curr = curr.next;
  }

  // 2. 翻转前 k 个节点
  let prev: ListNode | null = null;
  curr = head;
  for (let i = 0; i < k; i++) {
    const next = curr.next;
    curr.next = prev;
    prev = curr;
    curr = next;
  }

  // 3. 递归连接剩余部分
  head.next = reverseKGroup(curr, k);
  return prev; // 新头节点
}

复杂度:

  • 时间:O(n) —— 每个节点访问 2 次(检查 + 翻转);
  • 空间:O(1) —— 仅用常量额外空间(递归栈深度 O(n/k),题目通常允许)。

✅ 面试官关注点:边界处理(不足 k 个不翻转)、指针操作正确性、递归终止条件