前言
前面经过滴滴的两轮面试过后,又有大厂面试,这次是字节的一面,接下来是字节的面经
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,只渲染可见项。
关键步骤:
-
计算总高度:
totalHeight = itemHeight * itemCount(固定高)或累加动态高度数组; -
确定可视区范围:
scrollTop:当前滚动偏移;visibleCount = Math.ceil(containerHeight / itemHeight);startIndex = Math.floor(scrollTop / itemHeight);endIndex = startIndex + visibleCount + buffer(缓冲区防白屏);
-
渲染:
- 占位
div高度 =totalHeight; - 渲染
[startIndex, endIndex]内的真实子项; - 子项
top = startIndex * itemHeight定位;
- 占位
-
滚动监听:
onScroll更新scrollTop→ 触发 re-render; -
边界处理:
startIndex = Math.max(0, startIndex),endIndex = Math.min(itemCount, endIndex)。
优化点:
- 动态高度:用
Map缓存已知高度,二分查找起始索引; - 防抖滚动事件(但需注意:不能过度防抖,否则快速滚动卡顿);
- 使用
requestAnimationFrame同步渲染,避免 layout thrashing。
🧩 开源参考:
react-window、virtuoso、tiny-virtual-list。
4. 文章列表倾向用哪种图片形式?
取决于内容类型 & 交互需求:
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| 文章封面图(大图) | WebP | 体积小(比 JPG 小 25–35%),支持有损/无损,现代浏览器全覆盖 |
| Logo / 图标 / 透明背景 | PNG 或 SVG | PNG 支持 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 的区别
| 维度 | JPG | PNG |
|---|---|---|
| 压缩方式 | 有损(DCT 变换 + 量化) | 无损(DEFLATE 压缩) |
| 透明通道 | ❌ 不支持 | ✅ 支持 Alpha 通道 |
| 色彩深度 | 24-bit(RGB) | 24/32-bit(RGBA) |
| 文件大小 | 小(适合连续色调图像) | 大(适合边缘锐利图像) |
| 适用对象 | 照片、自然场景 | 图标、文字截图、带透明背景图 |
| 可编辑性 | 多次保存失真累积 | 无损保存,可反复编辑 |
⚠️ 误用后果:用 JPG 保存带文字的截图 → 锯齿/模糊;用 PNG 存高清照片 → 体积翻倍。
7. POST 和 GET 的适用场景与区别
| 特性 | GET | POST |
|---|---|---|
| 语义 | 获取资源 | 创建/修改资源 |
| 参数位置 | URL Query String | Request Body |
| 幂等性 | ✅ 是(多次请求结果相同) | ❌ 否(如创建订单会重复) |
| 缓存 | ✅ 可被浏览器/CDN 缓存 | ❌ 默认不缓存 |
| 安全性 | ❌ URL 可能被记录(日志/历史) | ✅ 相对安全 |
| 数据长度 | 受限(URL 长度 ≈ 2KB~8KB) | 无限制(服务端决定) |
| 编码 | application/x-www-form-urlencoded | application/json, multipart/form-data 等 |
✅ 正确用法:
- 获取数据 → GET;
- 提交表单/上传文件/修改状态 → POST;
- 删除资源 → DELETE;
- 上传文件必须用
POST + multipart/form-data。
8. 图片/文件如何上传?
流程:
-
获取文件:
const file = input.files[0]; -
构造 FormData:
const fd = new FormData(); fd.append('file', file); fd.append('name', file.name); // 可附加元数据 -
发送请求:
fetch('/upload', { method: 'POST', body: fd, // ⚠️ 不要设 headers!浏览器自动设为 multipart/form-data }); -
进度监控(高级) :
- 使用
XMLHttpRequest(支持upload.onprogress); - 或
ReadableStream+fetch(实验性,需 polyfill);
- 使用
-
分片上传(大文件) :
- 切片
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.userAgent或window.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事件;
- Hash 模式:
-
关键流程:
<Link to="/x">→ 调用history.push('/x');history执行window.history.pushState(null, '', '/x');- 浏览器触发
popstate事件; history.listen(callback)捕获事件 → 更新location;Router组件接收新location→ 触发 re-render;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 个不翻转)、指针操作正确性、递归终止条件