本轮要点: 本次面试主要考察校招生的基础内容
本轮共 13 道题。答案默认折叠,便于先自行作答。
1. 你是什么时候开始学前端的
题目要点
学习经历。
参考答案
我接触前端开发是在大学二年级的时候。当时,我选修了一门网页设计与开发的课程,这让我对前端开发产生了浓厚的兴趣。从那时起,我便开始系统地学习HTML、CSS和JavaScript等前端技术。我通过阅读书籍、在线教程和官方文档,打下了坚实的基础。同时,我积极参与实践项目,不断提升自己的技能。毕业后,我继续深入学习前端框架和工具,如Vue、React等,并在实际工作中不断积累经验。这段学习经历不仅让我掌握了前端开发的技术,还培养了我对前端开发的热爱和持续学习的态度。
2. 为什么选择前端这个方向呢,是如何学习前端的
题目要点
学习动机与方法。
参考答案
我选择前端开发是因为它结合了技术和创意,能够直接看到自己的代码转化为用户界面,这种即时反馈让我感到非常满足。同时,前端领域技术更新快,充满挑战,能够不断学习新知识。我通过在线课程(如Coursera、Udemy)、官方文档(如MDN Web Docs)和实践项目学习前端。我积极参与开源社区(如GitHub),与其他开发者交流经验,并通过阅读技术博客(如CSS-Tricks、JavaScript.info)保持对新技术的敏感度。这种多渠道的学习方式让我能够全面掌握前端开发的技能,并在实践中不断提升。
3. 说一下你觉得前端和后端有什么区别
题目要点
前后端理解。
参考答案
前端和后端在技术栈、运行环境、关注点等方面有显著区别:
- 技术栈:
- 前端:主要使用HTML、CSS、JavaScript及其框架(如Vue、React、Angular)进行开发。例如,使用Vue.js构建用户界面,处理用户交互逻辑。
- 后端:使用服务器端语言(如Node.js、Java、Python、Ruby)和框架(如Express、Spring Boot、Django、Rails)开发API、处理业务逻辑和与数据库交互。例如,使用Node.js和Express框架搭建RESTful API服务器。
- 运行环境:
- 前端:代码运行在客户端浏览器中,与用户直接交互。前端开发者需要考虑不同浏览器的兼容性和用户体验。
- 后端:代码运行在服务器上,处理请求、管理数据和执行业务逻辑。后端开发者关注服务器性能、安全性和可扩展性。
- 关注点:
- 前端:注重用户体验、界面设计、响应式布局和性能优化(如页面加载速度、交互流畅性)。前端开发者需要与设计师紧密合作,实现视觉效果和交互功能。
- 后端:关注数据存储、业务逻辑实现、API设计和安全性。后端开发者需要与数据库交互,处理并发请求,并确保系统的稳定性和可靠性。
- 开发工具:
- 前端:使用VS Code等编辑器,搭配浏览器开发者工具进行调试。依赖构建工具(如Vite、Webpack)进行代码打包和优化。
- 后端:使用IntelliJ IDEA、PyCharm等IDE,搭配数据库管理工具(如pgAdmin、MySQL Workbench)进行开发。需要配置服务器环境(如Nginx、Apache)和部署工具(如Docker、Kubernetes)。
- 数据处理:
- 前端:主要处理数据的展示和用户输入的收集。通过API从后端获取数据,并将其转换为用户界面。
- 后端:负责数据的存储、查询和业务逻辑处理。与数据库(如MySQL、MongoDB)交互,执行复杂的数据操作和事务管理。
- 性能优化:
- 前端:优化页面加载时间、减少重绘和回流、提升交互响应速度。例如,通过懒加载、代码分割、图片优化等技术提高性能。
- 后端:优化服务器响应时间、数据库查询效率、减少内存泄漏和提升并发处理能力。例如,使用缓存机制(如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 / restore、fillStyle、transform)本身是有成本的。引擎通常会在内部做状态缓存,避免重复设置相同的样式或变换。同时,对于复杂路径,可以提前构建 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(初始化、历史数据)
同步流程(典型)
- 客户端编辑 → 生成操作
- 本地立即应用(Optimistic Update)
- 通过 WS 发送到服务端
- 服务端校验 / 合并
- 广播给其他客户端
- 客户端应用远端操作
重连 & 离线
- 本地操作队列
- 带版本号 / 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 阅读器中非常常见的能力。
下面从 设计思路 → 常见实现方案 → 工程细节与坑 三个层次说明。
一、核心设计思路
滚动驱动状态,状态驱动目录高亮
关键问题只有两个:
- 如何判断当前“激活”的标题?
- 如何高效监听滚动并更新状态?
二、实现方案一(推荐):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(传统方案)
实现思路
- 监听 scroll
- 找到距离顶部最近但未超出的标题
示例代码
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%的文件大小,同时保持高质量。例如:
参考答案
- 优化图片格式:使用WebP和AVIF等现代格式,相比JPEG和PNG可减少30%-50%的文件大小,同时保持高质量。例如:
<img src="image.webp" alt="Description">
- 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">
- 响应式图片:使用
srcset和picture标签提供不同分辨率的图片,让浏览器根据设备特性选择合适的图片。例如:
<img data-src="image.webp" data-placeholder="placeholder.webp" alt="Description" class="lazy-load">
- 图片预加载:对于关键图片,使用
link[rel="preload"]提前加载。例如:
<link rel="preload" href="critical-image.webp" as="image">
- 占位符和渐进式加载:使用模糊占位符或渐进式JPEG,先加载低质量版本,再替换为高清版本。例如:
img.src = img.dataset.placeholder;
img.onload = () => {
img.src = img.dataset.src;
};
- 自动调整图片尺寸:在服务器端根据请求的尺寸动态调整图片大小,避免传输过大的图片。例如,使用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,并说明设计思路
题目要点
撤销 / 重做的本质是对状态时间线的管理;通过 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 万条数据表格的核心是控制渲染规模;必须基于行(必要时加列)虚拟化,将 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将对未来开发者产生深远影响,主要体现在以下几个方面:
- 提高开发效率:
- 代码生成:AI工具如GitHub Copilot和ChatGPT能够根据自然语言描述生成代码片段,减少重复性工作。例如,开发者只需描述功能需求,AI即可生成初始代码框架。
- 自动完成:AI驱动的IDE插件提供智能代码补全和重构建议,提升编码速度和质量。
- 增强应用智能:
- 自然语言处理:开发者可以轻松集成AI驱动的聊天机器人和语音助手,提升用户体验。例如,使用Dialogflow或Rasa构建智能客服系统。
- 图像和视频处理:AI库如TensorFlow.js和ML5.js使开发者能够在浏览器中实现实时图像识别和视频分析功能。
- 数据驱动的决策:
- AI分析工具:AI平台如Google Analytics和Mixpanel提供智能数据分析,帮助开发者理解用户行为并优化应用。
- 预测分析:AI模型预测用户需求和系统负载,提前调整资源分配。例如,使用AWS Forecast预测服务器流量。
- 降低技术门槛:
- 低代码/无代码平台:AI增强的低代码平台使非专业开发者能够构建复杂应用,拓展开发者的协作范围。
- 智能调试:AI工具如Sourcemap和Errorception自动分析错误日志,提供解决方案建议。
- 伦理和隐私挑战:
- 数据隐私:开发者需关注AI模型训练中的数据隐私保护,遵守法规如GDPR。
- 算法偏见:开发者需要评估和纠正AI模型中的偏见,确保应用公平性。
- 新的技术栈要求:
- 模型训练知识:开发者需了解基本的机器学习概念,以便更好地与数据科学家协作。
- AI优化技术:掌握模型压缩和量化技术,优化AI模型在前端的运行性能。
- 人机协作新模式:
- AI作为协作者:开发者与AI共同工作,AI提供建议,开发者进行创造性决策。
- 持续学习:开发者需不断学习AI新技术,保持技能的相关性。