第 1 轮 · 返回本次面经 · 已是最后一轮 →

本轮要点: 本次面试主要考察校招生的基础内容

本轮共 13 道题。答案默认折叠,便于先自行作答。

1. 你是什么时候开始学前端的

题目要点

学习经历。

参考答案

我接触前端开发是在大学二年级的时候。当时,我选修了一门网页设计与开发的课程,这让我对前端开发产生了浓厚的兴趣。从那时起,我便开始系统地学习HTML、CSS和JavaScript等前端技术。我通过阅读书籍、在线教程和官方文档,打下了坚实的基础。同时,我积极参与实践项目,不断提升自己的技能。毕业后,我继续深入学习前端框架和工具,如Vue、React等,并在实际工作中不断积累经验。这段学习经历不仅让我掌握了前端开发的技术,还培养了我对前端开发的热爱和持续学习的态度。

2. 为什么选择前端这个方向呢,是如何学习前端的

题目要点

学习动机与方法。

参考答案

我选择前端开发是因为它结合了技术和创意,能够直接看到自己的代码转化为用户界面,这种即时反馈让我感到非常满足。同时,前端领域技术更新快,充满挑战,能够不断学习新知识。我通过在线课程(如Coursera、Udemy)、官方文档(如MDN Web Docs)和实践项目学习前端。我积极参与开源社区(如GitHub),与其他开发者交流经验,并通过阅读技术博客(如CSS-Tricks、JavaScript.info)保持对新技术的敏感度。这种多渠道的学习方式让我能够全面掌握前端开发的技能,并在实践中不断提升。

3. 说一下你觉得前端和后端有什么区别

题目要点

前后端理解。

参考答案

前端和后端在技术栈、运行环境、关注点等方面有显著区别:

  1. 技术栈:
    1. 前端:主要使用HTML、CSS、JavaScript及其框架(如Vue、React、Angular)进行开发。例如,使用Vue.js构建用户界面,处理用户交互逻辑。
    2. 后端:使用服务器端语言(如Node.js、Java、Python、Ruby)和框架(如Express、Spring Boot、Django、Rails)开发API、处理业务逻辑和与数据库交互。例如,使用Node.js和Express框架搭建RESTful API服务器。
  2. 运行环境:
    1. 前端:代码运行在客户端浏览器中,与用户直接交互。前端开发者需要考虑不同浏览器的兼容性和用户体验。
    2. 后端:代码运行在服务器上,处理请求、管理数据和执行业务逻辑。后端开发者关注服务器性能、安全性和可扩展性。
  3. 关注点:
    1. 前端:注重用户体验、界面设计、响应式布局和性能优化(如页面加载速度、交互流畅性)。前端开发者需要与设计师紧密合作,实现视觉效果和交互功能。
    2. 后端:关注数据存储、业务逻辑实现、API设计和安全性。后端开发者需要与数据库交互,处理并发请求,并确保系统的稳定性和可靠性。
  4. 开发工具:
    1. 前端:使用VS Code等编辑器,搭配浏览器开发者工具进行调试。依赖构建工具(如Vite、Webpack)进行代码打包和优化。
    2. 后端:使用IntelliJ IDEA、PyCharm等IDE,搭配数据库管理工具(如pgAdmin、MySQL Workbench)进行开发。需要配置服务器环境(如Nginx、Apache)和部署工具(如Docker、Kubernetes)。
  5. 数据处理:
    1. 前端:主要处理数据的展示和用户输入的收集。通过API从后端获取数据,并将其转换为用户界面。
    2. 后端:负责数据的存储、查询和业务逻辑处理。与数据库(如MySQL、MongoDB)交互,执行复杂的数据操作和事务管理。
  6. 性能优化:
    1. 前端:优化页面加载时间、减少重绘和回流、提升交互响应速度。例如,通过懒加载、代码分割、图片优化等技术提高性能。
    2. 后端:优化服务器响应时间、数据库查询效率、减少内存泄漏和提升并发处理能力。例如,使用缓存机制(如Redis)、数据库索引优化、连接池管理等技术提高性能。

4. 最近有在看什么技术文章吗

题目要点

学习态度。

参考答案

最近我在阅读关于WebAssembly性能优化的文章,了解如何通过WebAssembly提升前端应用的运行效率。例如,我读了一篇关于使用WebAssembly进行图像处理的文章,介绍了如何将图像处理算法编译为WebAssembly模块,从而在浏览器中实现高性能的图像编辑功能。

同时,我也在关注前端框架的新特性,如Vue 3的Composition API和React的Concurrent Mode。这些新技术有助于构建更高效、响应更快的用户界面。

此外,我还阅读了一些关于前端安全的文章,特别是关于防止XSS攻击和CSRF攻击的最佳实践,这些知识对于构建安全的Web应用至关重要。

通过这些文章,我能够不断更新自己的知识体系,保持对技术前沿的敏感度,并将所学应用到实际项目中。

5. canvas 引擎有什么优化渲染性能的方案

题库原题:canvas 有什么优化渲染性能的方案

题目要点

Canvas 优化重点在减少重绘和状态切换;通过脏矩形或多层 Canvas 避免全量绘制;复用 Path2D 和绘制状态降低 API 调用成本;利用 rAF 合并渲染、Worker 和 OffscreenCanvas 解耦计算与绘制;通过资源缓存和分辨率控制平衡性能与清晰度。

参考答案

在 Canvas 场景中,性能瓶颈很少来自“API 是否足够快”,而更多来自渲染模型、状态管理和绘制策略是否合理。一个高性能的 Canvas 引擎,核心目标是:减少无效绘制、降低状态切换成本、把计算和绘制从主线程中解耦出来

首先要优化的是绘制次数本身。Canvas 是立即模式渲染,每一次 draw 都会直接作用在位图上,因此最重要的原则是避免全量重绘。常见做法是引入脏矩形机制,只重绘发生变化的区域;或者按层拆分 Canvas,例如将背景、静态元素和高频变化元素放在不同的 Canvas 中,静态层几乎不需要重绘,从而显著降低每帧绘制量。

其次是绘制路径和状态的复用。Canvas 的状态切换(如 save / restorefillStyletransform)本身是有成本的。引擎通常会在内部做状态缓存,避免重复设置相同的样式或变换。同时,对于复杂路径,可以提前构建 Path2D 对象并复用,而不是在每一帧中重复调用路径 API,这在图形数量较多时效果明显。

在渲染调度上,合理使用 requestAnimationFrame 是基本前提。引擎需要将多次状态变更合并到同一帧中执行,避免在一帧内多次触发绘制。进一步的优化是引入渲染队列或批处理机制,将相同类型的绘制操作集中处理,减少上下文切换。

对于计算密集型场景,还需要关注主线程负载。几何计算、碰撞检测、布局计算等可以放到 Web Worker 中执行,主线程只负责最终绘制。随着 OffscreenCanvas 的支持,甚至可以将部分绘制过程本身迁移到 Worker 中,从根本上降低对主线程的占用,这在复杂动画和大规模节点场景中尤为关键。

在资源层面,图片是常见的性能瓶颈。引擎通常会对图片资源进行预加载和尺寸适配,避免在绘制阶段触发解码和缩放;对于重复使用的位图,可以通过离屏 Canvas 缓存绘制结果,再在主 Canvas 中快速复用,相当于手动实现一层纹理缓存。

最后还需要关注分辨率与清晰度的平衡。直接按设备 DPR 放大 Canvas 尺寸虽然能提升清晰度,但会线性增加像素填充成本。成熟的引擎往往允许在性能和清晰度之间做动态取舍,例如在动画过程中降低渲染分辨率,在静态状态下再恢复高清绘制。

总体来看,Canvas 性能优化并不是单点技巧,而是一套围绕“少画、复用、分层、解耦”的系统性设计。

6. 如果让你设计一个协同文档的架构你会如何设计呢

题库原题:如果需要完成一个协同文档的系统,你会如何进行架构设计呢

题目要点

协同文档的核心在于并发一致性。

采用 CRDT 或 OT 保证多端编辑一致性,使用 WebSocket 实时同步操作,通过 快照 + 操作日志 做持久化,并在客户端做乐观更新、离线支持和光标协同,从而在一致性、性能和用户体验之间取得平衡。

参考答案

主要从 一致性模型、数据结构、同步协议、性能与扩展性 四个层面来设计。


一、总体架构分层

客户端(Web / App)

  • 文档编辑器(富文本 / 表格 / Markdown)
  • 本地状态管理(操作队列、版本号)
  • 冲突解决算法(OT / CRDT)
  • WebSocket 实时通信
  • 离线缓存与重连恢复

服务端

  • 协同引擎(OT / CRDT 处理)
  • 会话管理(在线用户、光标、选区)
  • 文档存储(快照 + 操作日志)
  • 权限与审计
  • 消息分发(广播变更)

存储层

  • 文档快照(Snapshot)
  • 操作日志(Op Log)
  • 历史版本 / 回滚

二、核心问题 1:并发编辑一致性

这是协同文档的技术核心

方案一:OT(Operational Transformation)

代表:Google Docs

思路

  • 所有编辑行为抽象成操作(insert / delete)
  • 服务端对并发操作进行 transform,保证顺序一致

优点

  • 算法成熟
  • 网络开销小

缺点

  • 实现复杂
  • 强依赖中心服务器
  • 对新类型文档扩展成本高

适合

  • 强中心化、文档模型相对稳定的系统

方案二:CRDT(Conflict-free Replicated Data Type)

代表:Yjs / Automerge / Figma

思路

  • 数据结构天然支持并发合并
  • 不需要复杂 transform
  • 最终一致性

优点

  • 天然支持离线
  • 无需中心化冲突解决
  • 客户端可直接合并

缺点

  • 数据结构复杂
  • 数据体积偏大
  • 实现成本高

适合

  • 多端、离线优先、弱中心化架构

👉 个人倾向:新项目优先 CRDT


三、核心问题 2:文档数据模型设计

文档抽象

Document {
  id
  content   // CRDT / AST / Delta
  version
  meta
}

操作抽象(以富文本为例)

Operation {
  type: insert | delete | format
  position
  value
  userId
  timestamp
}

存储策略

  • 快照 + 增量日志
  • 定期做 snapshot,避免回放全部操作
  • 支持版本回溯与历史对比

四、核心问题 3:实时同步机制

通信方式

  • WebSocket(主通道)
  • HTTP(初始化、历史数据)

同步流程(典型)

  1. 客户端编辑 → 生成操作
  2. 本地立即应用(Optimistic Update)
  3. 通过 WS 发送到服务端
  4. 服务端校验 / 合并
  5. 广播给其他客户端
  6. 客户端应用远端操作

重连 & 离线

  • 本地操作队列
  • 带版本号 / vector clock
  • 重连后补发 diff

五、核心问题 4:协同体验增强

光标与选区同步

  • 不进入文档主数据
  • 独立 channel 广播
  • 高频但可丢失

用户状态

  • 在线列表
  • 编辑中提示
  • 编辑锁(针对块级结构)

六、性能与扩展性设计

前端性能

  • 操作批量合并
  • 虚拟渲染(长文档)
  • diff 粒度控制(字符级 vs 块级)

后端扩展

  • 文档级 shard(同一文档落到同一实例)
  • Redis / 内存存活会话
  • Kafka / MQ 做变更广播

大文档优化

  • 分段 CRDT
  • 懒加载文档块
  • 局部同步

七、安全与权限

  • 文档级 / 块级权限
  • 操作校验(只读用户禁止 op)
  • 操作审计日志
  • 防止恶意刷操作

八、简化版技术选型示例

层级技术
编辑器Slate / ProseMirror
协同算法Yjs(CRDT)
通信WebSocket
后端Node.js / Go
存储PostgreSQL + Redis
消息Redis Pub/Sub / Kafka

7. 为什么选用了 IntersectionObserver 来做目录的自动高亮呢

题库原题:怎么根据当前滚动位置,实现目录中对应节点自动高亮?

题目要点

目录自动高亮的本质是监听正文滚动位置,判断当前阅读的标题节点。我通常优先使用 IntersectionObserver 监听标题进入视口,在性能和实现复杂度之间取得最佳平衡,同时配合锚点滚动和状态同步,保证目录与正文的双向联动。

参考答案

“目录中自动高亮”(TOC Active)本质是 根据当前滚动位置,判断用户正在阅读的标题节点,并同步更新目录状态。这是文档类产品、Markdown 阅读器中非常常见的能力。

下面从 设计思路 → 常见实现方案 → 工程细节与坑 三个层次说明。


一、核心设计思路

滚动驱动状态,状态驱动目录高亮

关键问题只有两个:

  1. 如何判断当前“激活”的标题?
  2. 如何高效监听滚动并更新状态?

二、实现方案一(推荐):IntersectionObserver

适用场景

  • 现代浏览器
  • 标题是标准 DOM(h1–h6)

实现思路

  • 监听所有标题元素
  • 哪个标题进入视口上方区域,即认为是当前阅读位置

基本实现示例

const headings = document.querySelectorAll('h1, h2, h3');

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        setActiveId(entry.target.id);
      }
    });
  },
  {
    rootMargin: '0px 0px -70% 0px',
    threshold: 0
  }
);

headings.forEach(h => observer.observe(h));

原理说明

  • rootMargin: -70%:让标题进入页面上半部分就触发
  • isIntersecting 表示元素进入“有效阅读区”
  • activeId 用于目录高亮

优点

  • 性能好(浏览器原生)
  • 不依赖 scroll 事件
  • 不需要手动计算高度

缺点

  • 老浏览器需 polyfill

三、实现方案二:scroll + getBoundingClientRect(传统方案)

实现思路

  1. 监听 scroll
  2. 找到距离顶部最近但未超出的标题

示例代码

const headings = [...document.querySelectorAll('h1,h2,h3')];

window.addEventListener('scroll', () => {
  let active = null;

  for (const h of headings) {
    const { top } = h.getBoundingClientRect();
    if (top <= 100) {
      active = h;
    } else {
      break;
    }
  }

  if (active) {
    setActiveId(active.id);
  }
});

优化点

  • requestAnimationFrame 节流
  • 提前缓存 headings
  • 滚动事件加 passive

优缺点

优点缺点
兼容性好性能较差
易理解手写逻辑多

四、实现方案三:基于文档模型(编辑器场景)

如果你是:

  • 使用 markdown-it
  • Slate / ProseMirror

可以直接从 AST / Node 结构获取标题位置

思路

  • 渲染时记录 headingId → offsetTop
  • 滚动时用 scrollTop + 二分查找

五、目录点击与滚动同步(反向联动)

点击目录 → 滚动正文

function scrollToHeading(id) {
  document.getElementById(id)?.scrollIntoView({
    behavior: 'smooth',
    block: 'start'
  });
}

注意点

  • 固定头部高度(navbar)
window.scrollTo({
  top: el.offsetTop - headerHeight,
  behavior: 'smooth'
});

六、工程级细节 & 常见坑

多个标题同时可见怎么办?

规则:

  • 取「最靠上的一个」
  • 或 IntersectionObserver 中按 boundingClientRect.top 排序

页面初始加载未触发 scroll?

  • 首次执行一次计算
  • 或 IntersectionObserver 自动触发

高亮抖动?

  • 增加缓冲区(rootMargin)
  • 或 debounce 状态更新

锚点重复?

  • 渲染 markdown 时生成 唯一 id
  • title + index 或 hash

七、React / Vue 实战建议

React

  • useEffect 注册 observer
  • activeId 放在 useState
  • 组件卸载时 disconnect

Vue

  • onMounted 注册
  • onUnmounted 清理
  • activeId 用 ref

八、推荐方案总结

场景推荐方案
普通文档IntersectionObserver
老浏览器scroll + rect
编辑器AST + offset

8. 如果需要做图片懒加载,并且图片数量很多,你会怎么做呢

题库原题:如果需要做图片懒加载,并且图片数量很多,你会怎么做呢?

题目要点

大量图片懒加载应优先使用 IntersectionObserver 作为触发机制;图片极多时需结合虚拟列表控制 DOM 数量;通过占位图、响应式图片和合适格式降低单图加载成本;同时做好兼容和资源清理,确保整体性能和稳定性。

参考答案

在图片数量很多的场景下,懒加载的目标不只是“晚一点加载”,而是在保证首屏体验的前提下,把网络、解码和渲染成本控制在可预测范围内。设计时通常需要从加载触发机制、资源策略以及工程层面的兜底方案三个层次来考虑。

首先,在加载触发机制上,IntersectionObserver 是首选方案。它由浏览器在渲染管线中统一调度,避免了大量 scroll 监听和同步布局计算。当图片元素即将进入视口时,再将真实图片地址从占位属性写回 src,可以做到按需、精准加载。对于图片非常密集的页面,通常会设置一个正向的 rootMargin,例如提前 200~300px 触发加载,保证用户滚动到图片位置时资源已经完成下载和解码,从而避免白屏或抖动。

当图片数量达到几百甚至上千时,仅靠懒加载还不够,需要配合虚拟列表(windowing)。思路是只在 DOM 中保留视口附近的图片节点,其余图片直接卸载,避免 DOM 数量过大导致的布局和内存压力。这在瀑布流、相册、长列表中非常关键,常见做法是将 IntersectionObserver 与虚拟滚动结合:虚拟列表控制节点数量,Observer 只负责触发资源加载。

在资源策略上,需要进一步降低单张图片的成本。通常会为懒加载配合 低质量占位图(LQIP)或模糊占位,例如先加载一个极小的 base64 或 WebP 预览图,再在真实图片加载完成后无缝替换。同时,应当利用现代图片能力,根据设备和网络条件选择合适格式和尺寸,例如通过 srcset / sizes 或后端图片服务,避免在移动端加载过大的原图。对关键图片(首屏、即将进入视口的图片)可以适度提高优先级,其余图片保持低优先级下载。

工程层面还需要考虑兜底和兼容性。对于不支持 IntersectionObserver 的环境,可以退化为节流后的 scroll 方案,但应限制监听逻辑的复杂度,避免在每次滚动中遍历大量节点。另外,需要处理图片加载失败、重复触发加载以及组件卸载后的 observer 清理问题,防止内存泄漏和无效请求。

综合来看,一个可扩展的方案通常是:虚拟列表控制数量 + IntersectionObserver 精准触发 + 响应式图片与占位策略降低单图成本。这样既能保证滚动流畅,也能在图片规模很大时保持稳定的性能表现。

9. 有考虑图片渲染上的优化吗?

题目要点

优化图片格式:使用WebP和AVIF等现代格式,相比JPEG和PNG可减少30%-50%的文件大小,同时保持高质量。例如:

参考答案
  1. 优化图片格式:使用WebP和AVIF等现代格式,相比JPEG和PNG可减少30%-50%的文件大小,同时保持高质量。例如:
<img src="image.webp" alt="Description">
  1. CDN加速:使用CDN分发图片,减少服务器负载,提升全球访问速度。例如,将图片存储在Cloudflare或Akamai等CDN服务上。
<img srcset="image-small.webp 500w, image-medium.webp 1000w, image-large.webp 2000w"
     sizes="(max-width: 600px) 500px, (max-width: 1200px) 1000px, 2000px"
     src="image-medium.webp" alt="Description">
  1. 响应式图片:使用srcsetpicture标签提供不同分辨率的图片,让浏览器根据设备特性选择合适的图片。例如:
<img data-src="image.webp" data-placeholder="placeholder.webp" alt="Description" class="lazy-load">
  1. 图片预加载:对于关键图片,使用link[rel="preload"]提前加载。例如:
<link rel="preload" href="critical-image.webp" as="image">
  1. 占位符和渐进式加载:使用模糊占位符或渐进式JPEG,先加载低质量版本,再替换为高清版本。例如:
img.src = img.dataset.placeholder;
img.onload = () => {
    img.src = img.dataset.src;
};
  1. 自动调整图片尺寸:在服务器端根据请求的尺寸动态调整图片大小,避免传输过大的图片。例如,使用Sharp库在Node.js中处理图片:
const sharp = require('sharp');
app.get('/resize/:width/:height/:image', (req, res) => {
    const { width, height, image } = req.params;
    sharp(`original/${image}`)
        .resize(parseInt(width), parseInt(height))
        .toFile(`resized/${width}x${height}-${image}`)
        .then(() => {
            res.sendFile(`resized/${width}x${height}-${image}`);
        });
});

10. 实现一个支持撤销/重做功能的React Hook,并说明设计思路

题库原题:实现一个支持撤销/重做功能的React Hook,并说明设计思路

题目要点

撤销 / 重做的本质是对状态时间线的管理;通过 past / present / future 的三段式模型,可以用 O(1) 的方式完成撤销和重做;在 React 中用自定义 Hook 封装这一模型,结合函数式 setState,既保证了可预测性,也便于在复杂编辑场景中复用和扩展。

参考答案

实现“撤销 / 重做”的核心是状态历史的建模方式

下面从 设计思路 → 数据结构 → Hook 实现 → 使用与边界 四个层次说明。


一、核心设计思路

撤销 / 重做问题,本质是对状态时间线的管理:

  • 撤销:回到上一个状态
  • 重做:回到下一个状态
  • 新操作发生时:未来状态失效

因此不能只保存“当前值”,而需要显式维护过去、现在、未来三段。

一个成熟的设计目标通常包括:

  • 操作是纯同步、可预测的
  • 撤销 / 重做是 O(1)
  • 不依赖具体业务数据结构
  • 与 React 更新模型天然契合

二、状态模型设计

最常见、也是最稳定的模型是三段式结构:

{
  past: T[]        // 历史状态
  present: T       // 当前状态
  future: T[]      // 可重做状态
}

状态流转规则

  • set

    • past += present
    • present = newState
    • future 清空
  • undo

    • future.unshift(present)
    • present = past.pop()
  • redo

    • past.push(present)
    • present = future.shift()

这是 Redux Undo/Redo、编辑器、协同系统中最常见的模型。


三、Hook 的实现

import { useCallback, useState } from 'react';

interface HistoryState<T> {
  past: T[];
  present: T;
  future: T[];
}

export function useUndoRedo<T>(initialValue: T) {
  const [state, setState] = useState<HistoryState<T>>({
    past: [],
    present: initialValue,
    future: [],
  });

  const set = useCallback((value: T) => {
    setState(prev => ({
      past: [...prev.past, prev.present],
      present: value,
      future: [],
    }));
  }, []);

  const undo = useCallback(() => {
    setState(prev => {
      if (prev.past.length === 0) return prev;

      const previous = prev.past[prev.past.length - 1];
      const newPast = prev.past.slice(0, -1);

      return {
        past: newPast,
        present: previous,
        future: [prev.present, ...prev.future],
      };
    });
  }, []);

  const redo = useCallback(() => {
    setState(prev => {
      if (prev.future.length === 0) return prev;

      const next = prev.future[0];
      const newFuture = prev.future.slice(1);

      return {
        past: [...prev.past, prev.present],
        present: next,
        future: newFuture,
      };
    });
  }, []);

  return {
    state: state.present,
    set,
    undo,
    redo,
    canUndo: state.past.length > 0,
    canRedo: state.future.length > 0,
  };
}

四、使用示例

const {
  state,
  set,
  undo,
  redo,
  canUndo,
  canRedo
} = useUndoRedo('');

<input
  value={state}
  onChange={e => set(e.target.value)}
/>

<button disabled={!canUndo} onClick={undo}>撤销</button>
<button disabled={!canRedo} onClick={redo}>重做</button>

五、关键设计点说明(面试重点)

为什么不用数组直接存所有历史?

因为撤销 / 重做不是线性回放,而是“时间分叉”问题。 三段式模型能明确区分“已发生”和“可重做”。


为什么新 set 要清空 future?

因为一旦产生新操作,原本的“未来时间线”已经不再成立,这是编辑器领域的基本约定。


为什么用函数式 setState?

确保在高频更新、并发渲染(Concurrent Mode)下状态是安全的,避免闭包读取旧 state。


六、工程级优化点(进阶)

限制历史长度(防止内存增长)

const MAX = 50;

past: [...prev.past, prev.present].slice(-MAX)

支持函数式更新(类似 setState)

const set = (updater: T | ((prev: T) => T)) => {
  setState(prev => {
    const next =
      typeof updater === 'function'
        ? (updater as any)(prev.present)
        : updater;

    return {
      past: [...prev.past, prev.present],
      present: next,
      future: [],
    };
  });
};

支持批量合并(如输入法、拖拽)

  • debounce set
  • 或提供 commit() API

七、适用与不适用场景

适用:

  • 表单编辑
  • 富文本 / 画布
  • 配置面板
  • 低频但可回退的用户操作

不适用:

  • 高频实时数据(如动画帧)
  • 大对象深拷贝成本极高的场景(需结构共享)

11. 现在有一个需要支持10万条数据渲染的表格组件,你会如何设计和优化?

题库原题:有一个需要支持10万条数据渲染的表格组件,你会如何设计和优化?

题目要点

10 万条数据表格的核心是控制渲染规模;必须基于行(必要时加列)虚拟化,将 DOM 数量限制在可视区范围;数据处理与渲染解耦,重计算放在数据层甚至 Worker 中;通过稳定 key、行级 memo 和简化布局,确保滚动和交互始终流畅。

参考答案

问题的本质已经是如何控制渲染规模、更新成本和交互响应时间

设计时应当从数据层、渲染层和交互层协同入手,而不是寄希望于某一个单点优化。


一、整体设计原则

核心原则只有一个:任何时刻参与渲染和布局计算的 DOM 数量必须是可控的。 10 万条数据不可能全部进入真实 DOM,否则无论是首屏渲染、滚动还是重排都会不可接受。

因此,设计目标通常包括:

  • 首屏渲染在毫秒级完成
  • 滚动过程中不触发布局抖动
  • 排序、筛选等操作不阻塞主线程
  • 单行更新不导致整表重渲染

二、渲染层:虚拟化是前提,不是优化项

1. 行虚拟化(Windowing)

表格只渲染视口内 + buffer 区域的行,其余数据只存在于内存中。

基本思路是:

  • 通过 scrollTop 计算当前可见的起始索引和结束索引
  • 只渲染这一区间的数据
  • 用一个占位容器(totalHeight)模拟完整滚动高度

这一层可以自行实现,也可以基于成熟方案,例如 react-window / react-virtualized 的核心思想。

2. 固定高度 vs 动态高度

  • 固定行高:计算简单,性能最好,适合大多数业务表格
  • 动态行高:需要行高缓存 + 二分查找,复杂度明显上升,应尽量避免

在 10 万量级下,优先设计为固定行高。


三、列与布局层面的优化

1. 列虚拟化(必要时)

当列数非常多(例如 50+)时,需要同时做横向虚拟化,否则单行 DOM 过多同样会拖慢布局和绘制。

2. 减少布局复杂度

  • 避免 table 原生布局计算,优先使用 div + flex / grid
  • 避免每个单元格内存在复杂嵌套结构
  • 能静态的样式不依赖 JS 计算

四、数据与计算层优化

1. 排序、过滤、搜索不直接作用于渲染层

这些操作应当:

  • 在数据层完成
  • 返回新的数据引用
  • 再交由虚拟列表渲染

对于复杂规则:

  • 使用 Web Worker 做计算
  • 主线程只负责渲染结果

2. 结构共享与不可变数据

  • 行数据保持稳定引用
  • 未变更的行不重新创建对象
  • 配合 React.memo / useCallback,保证单行更新不扩散

五、React 组件级别的控制

1. 行级组件隔离

每一行都是独立组件,且具备明确的 memo 边界:

  • 行只依赖当前行数据
  • 列配置尽量外提,避免每次 render 生成新对象

2. key 的稳定性

  • key 必须来自稳定主键
  • 禁止使用 index 否则在虚拟化场景下会导致大量不必要的卸载与重建。

六、滚动与交互体验优化

1. 滚动事件处理

  • 使用容器 scroll,而非 window scroll
  • 通过 requestAnimationFrame 或同步计算,避免额外 setState 抖动

2. 选中、高亮等状态设计

  • 状态尽量用 Set / Map 结构存储
  • 判断逻辑在行内部完成
  • 避免将大规模选中状态直接挂在表格根组件

七、进一步的工程级优化(高阶)

  • 首屏分批渲染(Idle Callback)
  • 非关键列延迟挂载
  • 使用 Canvas 渲染只读表格(极端场景)
  • SSR 场景下只输出首屏数据

八、设计取舍与边界说明

需要明确的是: 支持 10 万条数据并不等于一次性操作 10 万条 DOM

合理的设计,是让用户“感觉在操作 10 万条数据”,而系统实际只处理几十到几百行的渲染工作。

12. 有了解 AI 的使用和一些前沿概念吗

题库原题:有了解 AI 的使用和一些前沿概念吗

题目要点

对 AI 的理解更偏工程化与落地视角;在研发侧用于提效和知识沉淀,在产品侧用于搜索、配置和交互增强;关注 RAG、Agent、流式推理等前沿概念;前端的核心价值正在从实现细节转向系统约束、交互设计和体验管理。

参考答案

对 AI 的理解通常不应该停留在“会不会调接口”,而是放在 能力边界、工程化落地方式以及对现有系统的影响 上。


一、AI 在工程中的实际使用

在实际项目中,AI 更多是作为一种 能力增强层 而不是核心业务逻辑。

在研发侧,主要体现在三个方向: 一是辅助编码与重构,通过大模型完成样板代码生成、接口联调、测试用例补全,但需要结合静态分析和代码规范进行二次约束,避免不可控输出进入主干分支。 二是文档与知识体系构建,例如从代码、PR、Issue 中自动抽取结构化知识,反向服务于搜索和新人 onboarding,本质是将非结构化信息转为可检索资产。 三是智能工具链,例如将 AI 接入脚手架、低代码平台或编辑器插件,使其在明确上下文下工作,而不是开放式对话。


二、前端相关的 AI 应用方向

在偏前端或产品层,AI 的落点通常更偏“交互增强”:

  • 智能搜索与推荐:从关键词匹配转向语义理解,尤其在文档站、配置后台、复杂表单中,能显著降低使用门槛。
  • 表单与配置生成:基于自然语言生成结构化 schema,再交由 Form / UI 引擎渲染,前端的价值转向校验、约束和可视化。
  • 多模态交互:语音、图片、文本混合输入,对前端意味着状态管理、异步流式渲染和错误兜底能力要更强。

三、对前沿概念的理解

从近一两年的技术趋势来看,有几个概念值得重点关注:

RAG(Retrieval Augmented Generation) 通过“检索 + 生成”解决大模型幻觉问题。工程上更像是一个“受控上下文构建系统”,前端往往承担结果可解释性和引用来源展示的责任。

Agent 与 Tool Calling 模型不再只是回答问题,而是能拆解任务、调用工具、迭代执行。对前端来说,意味着 UI 不再只是输入输出,而是要承载任务进度、状态机和中间结果。

流式推理与增量渲染 模型输出从一次性结果变为 token 流,对前端架构提出了新的要求,包括可中断渲染、部分结果回滚以及更细粒度的 loading 状态管理。

模型能力与系统约束的边界 工程实践中越来越强调“用规则兜底,用模型补全”,而不是完全依赖模型决策。这一点在权限、资金、配置类场景尤为重要。


四、对前端角色变化的看法

AI 并不会削弱前端工程师的价值,但会明显改变关注重点:

  • 从“页面实现”转向“系统交互设计”
  • 从“写组件”转向“约束与编排能力”
  • 从“单次交互”转向“长期上下文体验”

谁能把 AI 放进一个可控、可维护、可演进的系统里,谁就具备长期优势。

13. 你觉得在未来 AI 对于开发者会有什么影响呢

题目要点

技术视野。

参考答案

AI将对未来开发者产生深远影响,主要体现在以下几个方面:

  1. 提高开发效率:
    1. 代码生成:AI工具如GitHub Copilot和ChatGPT能够根据自然语言描述生成代码片段,减少重复性工作。例如,开发者只需描述功能需求,AI即可生成初始代码框架。
    2. 自动完成:AI驱动的IDE插件提供智能代码补全和重构建议,提升编码速度和质量。
  2. 增强应用智能:
    1. 自然语言处理:开发者可以轻松集成AI驱动的聊天机器人和语音助手,提升用户体验。例如,使用Dialogflow或Rasa构建智能客服系统。
    2. 图像和视频处理:AI库如TensorFlow.js和ML5.js使开发者能够在浏览器中实现实时图像识别和视频分析功能。
  3. 数据驱动的决策:
    1. AI分析工具:AI平台如Google Analytics和Mixpanel提供智能数据分析,帮助开发者理解用户行为并优化应用。
    2. 预测分析:AI模型预测用户需求和系统负载,提前调整资源分配。例如,使用AWS Forecast预测服务器流量。
  4. 降低技术门槛:
    1. 低代码/无代码平台:AI增强的低代码平台使非专业开发者能够构建复杂应用,拓展开发者的协作范围。
    2. 智能调试:AI工具如Sourcemap和Errorception自动分析错误日志,提供解决方案建议。
  5. 伦理和隐私挑战:
    1. 数据隐私:开发者需关注AI模型训练中的数据隐私保护,遵守法规如GDPR。
    2. 算法偏见:开发者需要评估和纠正AI模型中的偏见,确保应用公平性。
  6. 新的技术栈要求:
    1. 模型训练知识:开发者需了解基本的机器学习概念,以便更好地与数据科学家协作。
    2. AI优化技术:掌握模型压缩和量化技术,优化AI模型在前端的运行性能。
  7. 人机协作新模式:
    1. AI作为协作者:开发者与AI共同工作,AI提供建议,开发者进行创造性决策。
    2. 持续学习:开发者需不断学习AI新技术,保持技能的相关性。

第 1 轮 · 返回本次面经 · 已是最后一轮 →