前端真题 130 题 · 性能网络工程化算法
缓存策略、白屏、RAIL、Tree Shaking、重排重绘、浏览器架构、HTTP/2/3、TCP/UDP、Webpack、微前端、算法题等 49-79 题。
49. 缓存策略
HTTP 缓存分强缓存与协商缓存两级:
① 强缓存(不发请求,直接用本地)
Cache-Control(HTTP/1.1 优先):max-age=3600、public/private、no-cache(仍会协商)、no-store(完全不缓存)、immutable;Expires(HTTP/1.0,绝对时间,已不推荐)。
② 协商缓存(发请求,服务器决定是否用缓存)
- Last-Modified / If-Modified-Since(时间维度):服务器对比文件修改时间,未变返回 304;
- ETag / If-None-Match(内容指纹,优先级更高、更精准):对比 hash,未变 304。
请求流程:命中强缓存 → 200(from disk/memory cache),不发请求;未命中强缓存但 ETag 匹配 → 304,服务器不返回 body。
最佳实践:
- HTML:
no-cache(需最新); - 静态资源(JS/CSS/图片):
Cache-Control: max-age=31536000, immutable+ 文件名带 hash(内容变则文件名变 → 天然更新); - CDN 层 cache-control 配合。
考察点:两级缓存区别、304 与 200 差异、no-cache vs no-store、hash 文件名更新策略、Service Worker 缓存(PWA 可控缓存)。
HTTP 缓存决策树图解:
flowchart TD
Req["发起请求"] --> Strong{"命中强缓存?<br/>Cache-Control: max-age<br/>/ Expires"}
Strong -->|命中| FromCache["直接读本地缓存<br/>200 (from disk/memory cache)<br/><b>不发请求</b>"]
Strong -->|未命中 / 已过期| Negotiate["发起协商缓存请求<br/>带上 If-None-Match / If-Modified-Since"]
Negotiate --> Check{"服务端比对<br/>ETag / Last-Modified"}
Check -->|没变| N304["<b>304 Not Modified</b><br/>只返回响应头,不传 body"]
Check -->|已变| N200["<b>200 OK</b><br/>返回新资源 + 新缓存头"]
N304 --> UseLocal["用本地缓存"]
N200 --> UpdateLocal["写入新缓存"]
style FromCache fill:#d5e8d4,stroke:#82b366
style N304 fill:#fff2cc,stroke:#d6b656
style N200 fill:#dae8fc,stroke:#6c8ebf
读图要点:强缓存不发请求,协商缓存要发一次请求但可能省下 body 传输。
高频追问:no-cache是"可以缓存但要协商"(≠ 不缓存);no-store才是"完全不缓存"。生产实践一般是 HTML 用协商缓存、带 hash 的静态资源用强缓存 + 一年过期。
50. 白屏
页面加载时用户看到一片空白(FCP 前)的优化问题。白屏成因 + 对策:
成因:
- CSS/JS 阻塞:
<script>在 head 同步执行阻塞 DOM 解析;CSS 未加载完阻塞渲染; - 首屏体积过大:JS 包太大,下载+解析执行时间长 → 首屏迟迟不渲染;
- 字体加载阻塞(FOIT);接口依赖首屏(组件等数据才渲染);
- 框架初始化重、错误抛错导致整树不渲染。
对策:
- 关键 CSS 内联,非关键异步;
<script>加defer/async或移到 body 尾; - 代码分割(路由级 chunk、按需加载)+ tree shaking,减首屏 JS;
- 骨架屏/loading(先出壳再填充);
- SSR/预渲染(服务端出 HTML);
- 字体
font-display: swap; - 监控:FCP、白屏时长埋点,用 Performance/Lighthouse 定位;错误边界兜底,避免整页白。
回答亮点:说"白屏不是一个指标而是现象",落到 FCP/LCP 衡量;能结合自己项目给出具体优化动作与前后数据。
51. 前端性能优化指标 RAIL
RAIL 是 Google 提出的以用户为中心的性能模型,四个维度:
- R - Response(响应):输入到反馈 < 100ms(事件处理);
- A - Animation(动画):每帧 < 16ms(60fps),帧内无长任务阻塞;
- I - Idle(空闲):主线程空闲块 ≥ 50ms,用来延迟非关键工作(空闲时执行);
- L - Load(加载):首屏内容 < 5s(弱网),并尽早给用户反馈。
落地手段:
- Response:事件监听拆分(防抖/节流),避免在 handler 里跑重活;
- Animation:rAF、transform/opacity;
- Idle:
requestIdleCallback分批执行低优任务; - Load:CRP 优化、懒加载、压缩。
关联指标(加分):与 Web Vitals(LCP / INP / CLS)和 Lighthouse 评分体系的关系;实践中用 Performance 面板 + DevTools 的 "Performance insights" 定位。
⚠️ 勿答 FID:Google 已于 2024 年 3 月用 INP(Interaction to Next Paint)取代 FID(First Input Delay),FID 不再是 Core Web Vital。现在三大核心指标就是 LCP、INP、CLS。答 FID 会被认为知识没更新。
52. Tree Shaking
Tree Shaking(摇树):打包时移除未被引用的模块代码,减小产物体积。前提是 ES Module 的静态结构(import/export 顶层、不能动态/条件导出)——因为依赖关系编译期可分析。
原理:webpack/rollup 先把模块解析成 AST → 标记"被使用"的导出 → 删除未被引用的 export(配合 sideEffects: false 声明模块无副作用,可放心删)。
// utils.js
export const a = 1;
export const b = 2; // 若没人 import b,b 会被 shake 掉
// 使用
import { a } from './utils';
实践注意:
- 必须是 ESM 源码(
import/export),CommonJSrequire无法静态分析 → 库要同时发 ESM 包(package.jsonmodule字段 / exports); sideEffects声明:CSS/全局样式等有副作用的文件要在 sideEffects 白名单,否则被误删;- babel/tsconfig 别把模块转成 CJS(
preset-env的 modules:false); - webpack 生产模式默认开;rollup/esbuild/vite 天然支持。
考察点:为什么 ESM 才行(静态);sideEffects 作用;配合分包(splitChunks)减少重复。
53. 重排和重绘
回流/重排(Reflow/Layout):几何属性(尺寸、位置)变化 → 重新计算布局树,成本高(整棵或局部树重算)。
重绘(Repaint/Paint):外观(颜色、阴影、visibility)变化不涉及几何 → 重新绘制,成本较低但也要避免频繁。
触发重排的操作:
- 增删改 DOM 节点、改变窗口大小、改 font-size、
offsetWidth/scrollTop等读取布局属性(可能强制同步布局); - 改 width/height/margin/padding/position/top/left/display 等。
触发重绘但不重排:color/background-color/visibility/box-shadow/outline(不改变布局)。
优化:
- 批量修改 DOM(DocumentFragment、display:none 后改再显示、className 切换);
- 缓存布局属性读取(避免读写交替的 layout thrashing);
- 用
transform/opacity动画(合成层,完全不触发重排重绘); - 减小 DOM 规模与层级;
- 必要时
will-change/contain。
考察点:能举例区分两者触发条件;说出"读 offsetWidth 会强制刷新布局"这种性能坑;现代优化聚焦"合成"(composite)概念。
重排 / 重绘 / 合成三条路径图解:
flowchart TD
Change["修改样式 / 属性"] --> Which{"改的是什么?"}
Which -->|"几何属性<br/>width / height / margin<br/>font-size / display"| Layout["<b>重排 Reflow</b><br/>重新计算布局"]
Layout --> Paint1["重绘 Paint"]
Paint1 --> Composite1["合成 Composite"]
Which -->|"绘制属性<br/>color / background<br/>visibility / box-shadow"| Paint2["<b>重绘 Paint</b><br/>(跳过布局)"]
Paint2 --> Composite2["合成 Composite"]
Which -->|"transform / opacity<br/>(仅在合成层)"| Composite3["<b>仅合成 Composite</b><br/>交 GPU,可 60fps"]
style Layout fill:#f8cecc,stroke:#b85450
style Paint2 fill:#fff2cc,stroke:#d6b656
style Composite3 fill:#d5e8d4,stroke:#82b366
读图要点:代价 重排 > 重绘 > 合成。动画优先用
transform/opacity,并配合will-change提升到合成层。
高频追问:什么是强制同步布局(layout thrashing)——在同一帧里"写样式 → 紧接着读offsetWidth",浏览器被迫立刻同步重排;解法是批量写、再批量读。
54. 前端性能优化手段
综合题,建议按"加载性能 → 渲染性能 → 运行时性能"组织答案:
加载性能:
- 资源体积:代码分割、tree-shaking、压缩(gzip/br)、图片 WebP/AVIF + 响应式、按需加载(路由/组件/图标);
- 网络:CDN、HTTP/2/3、预连接
dns-prefetch/preconnect、关键资源preload、懒加载; - 缓存:强缓存 + hash 文件名、Service Worker 离线缓存;
- 首屏:关键 CSS 内联、SSR/预渲染、骨架屏。
渲染性能:
- 减少 DOM 层级与节点数;避免重排重绘(上题手段);
- 动画走合成(transform/opacity);
content-visibility跳过屏外; - 滚动/输入用 rAF + 防抖节流。
运行时与体验:
- 长任务拆分(
requestIdleCallback/调度)、避免主线程阻塞; - 内存:避免泄漏、及时释放闭包/监听器;
- 指标驱动:以 Web Vitals(LCP、INP、CLS)+ Lighthouse 量化验收。
加分:结合一个真实案例讲"做了什么 → 指标从多少到多少"。
55. 渲染合成层
合成(Composite)是渲染最后一步:浏览器把页面拆成若干图层(layers),分别绘制(paint)后由 GPU 合成到屏幕。不改变层内容、只移动/变换层的动画,不需要重新 layout/paint,只做合成 → 性能最好。
会创建独立图层(合成层)的情况:
transform/opacity动画中(或will-change: transform/opacity);position: fixed、video/canvas/iframe元素、filter、3D transform(translateZ(0)曾被用来强制提层);overflow滚动容器(整层滚动)。
为什么动画卡:若属性触发 layout/paint(如 left),每帧都要主线程重排+绘制,再交给合成;若只动 transform,主线程只需移动层 → GPU 合成,帧率稳定。
代价:合成层过多会占用 GPU 内存(尤其移动端),且层数多时合成开销大。不要无脑 translateZ(0) 提层;动画结束可移除 will-change。
考察点:说出"分层 → 光栅化 → 合成"流程;解释 transform/opacity 快的原理;提 will-change 的正确用法与滥用后果。
56. 浏览器架构
现代浏览器(Chrome)采用多进程架构:
- 浏览器进程(Browser/UI 进程):管理地址栏、书签、各进程协调、网络请求(网络线程也常在独立 network 进程);
- GPU 进程:负责合成与绘制、GPU 资源;
- 网络进程:网络栈;
- 渲染进程(Renderer,每标签页一个,受站点隔离影响可能一进程多标签或每站点一组):负责 HTML/CSS/JS 解析渲染执行、事件、定时器;包含主线程(JS 执行、布局、绘制)、合成线程、Web Worker 线程、光栅线程;
⚠️ 细节纠偏:Service Worker 运行在独立的 Service Worker 进程中,不在渲染进程里。渲染进程内的 Worker 是 Web Worker。
- 插件进程。
渲染进程内的线程分工:
- 主线程:解析 HTML/CSS、执行 JS、Layout、Paint 记录 → 最忙;
- 合成线程(Compositor):接收绘制指令分块、滚动时移动层、把层交给 GPU——滚动手势若只涉及合成则主线程不参与(快速滚动);
- 光栅线程:把层光栅化为位图。
价值:多进程 → 隔离(一个标签崩溃/卡顿不影响整个浏览器、站点隔离安全)、更好利用多核;代价是内存开销。站点隔离(Site Isolation) 让不同站点不同进程,增强安全。
考察点:能画进程/线程图;JS 单线程在主线程;Web Worker 与主线程关系(多线程并发但无 DOM);为何渲染进程崩溃可恢复。
57. 进程和线程
- 进程:操作系统资源分配的最小单位,拥有独立内存空间/文件句柄;进程间互相隔离,通信靠 IPC(管道、消息队列、共享内存、socket 等);
- 线程:CPU 调度的最小单位,是进程内的执行单元;同一进程的线程共享内存空间,可并发执行,但需同步(锁/信号量)防竞态;
- 关系:一个进程可含多个线程;线程崩溃可能拖垮整个进程,进程崩溃一般不影响其他进程。
JS 视角:JS 主线程单线程(避免共享内存竞态与加锁复杂性),异步靠事件循环;Web Worker 提供多线程能力(PostMessage 通信、无法直接操作 DOM);浏览器渲染进程多线程(主线程/合成线程/Worker)。
前端考点:单线程为何不卡(异步非阻塞 + 事件循环);Worker 传数据的结构化克隆/Transferable(transfer 零拷贝)。
58. HTTP/2.0
HTTP/2 相对 1.1 的核心改进:
- 二进制分帧:1.1 是文本协议,2.0 把消息切成二进制帧(HEADERS/DATA),更高效解析;
- 多路复用(Multiplexing):一个 TCP 连接上并发多个请求/响应流(stream),互不阻塞——解决 1.1 的队头阻塞(每连接串行、浏览器约 6 连接上限);
- 头部压缩(HPACK):用静态表 + 动态表 + Huffman 压缩请求头,减少重复头传输;
- 服务器推送(Server Push):服务器可主动推资源——因实现复杂、缓存控制难,Chrome 已移除此特性,勿重点吹;
- 优先级与流控制:可设资源加载优先级。
仍存在的问题:TCP 层面的队头阻塞(丢包时整个连接等待重传);需要 TLS(实际部署 2.0 基本都加密)。
考察点:1.1 vs 2.0 对比表;"为什么还要 3.0"的引出;多路复用如何解决连接数瓶颈。
59. HTTP/3.0
核心:把传输层从 TCP 换成 QUIC(基于 UDP),解决 HTTP/2 的 TCP 队头阻塞。
改进点:
- 基于 UDP + QUIC:QUIC 在用户态实现可靠传输、拥塞控制、TLS1.3 集成(0-RTT/1-RTT 握手,建连快);
- 无队头阻塞:TCP 丢一个包会阻塞后续所有流;QUIC 每个流独立,丢包只影响该流;
- 连接迁移:连接用 Connection ID 标识,切换网络(WiFi→4G)不断连;
- 0-RTT 恢复:复用缓存密钥时首包即可带数据,弱网/移动端体验提升;
- 内建前向纠错、加密默认(TLS1.3)。
代价/限制:UDP 在部分网络被限速/拦截;部署需 CDN/服务器支持;目前主流浏览器已支持 h3。
对比记忆:HTTP/1.1 文本+串行 → HTTP/2 二进制+多路复用(仍 TCP 队头阻塞)→ HTTP/3 QUIC/UDP 彻底去队头阻塞 + 更快握手。
考察点:说清"2.0 的队头阻塞来自 TCP,3.0 换 UDP/QUIC 解决";0-RTT 握手含义;QUIC 连接迁移亮点。
60. TCP
TCP(传输控制协议):面向连接、可靠、字节流传输的协议。三要素:三次握手建立、四次挥手断开、确认重传保可靠。
三次握手(建立连接,双方确认收发能力):
- 客户端发
SYN(seq=x); - 服务端回
SYN+ACK(seq=y, ack=x+1); - 客户端发
ACK(ack=y+1)——此后可传数据。
为什么三次:两次无法确认客户端已收到服务端能力(防止已失效连接请求突然到达造成资源浪费/错误建立)。
四次挥手(断开连接,因全双工需各自关闭):
- 主动方发
FIN;2. 被动方回ACK;3. 被动方再发FIN;4. 主动方回ACK,等待 2MSL 后关闭。
可靠机制:
- 确认应答 ACK + 超时重传;滑动窗口(流量控制);拥塞控制(慢启动、拥塞避免、快重传、快恢复);
- 有序交付(序号机制)、校验和。
面试结合前端:为什么 HTTP/1.1 慢(队头阻塞、连接数限制)→ HTTP/2 多路复用仍受 TCP 队头阻塞 → HTTP/3 用 QUIC/UDP。
TCP 建连与断连图解:
sequenceDiagram
autonumber
participant C as 客户端
participant S as 服务端
rect rgb(213, 232, 212)
Note over C,S: 三次握手(建立连接)
C->>S: SYN, seq=x
S->>C: SYN+ACK, seq=y, ack=x+1
C->>S: ACK, ack=y+1
Note over C,S: 连接建立,可传数据
end
rect rgb(255, 230, 204)
Note over C,S: 四次挥手(断开连接)
C->>S: FIN, seq=u
S->>C: ACK, ack=u+1
Note over S: 服务端可能还有数据要发<br/>(半关闭状态)
S->>C: FIN, seq=w
C->>S: ACK, ack=w+1
Note over C: 等待 2MSL 后彻底关闭
end
读图要点:握手 3 次是为了双方都确认收发能力(2 次不够,4 次多余);挥手 4 次是因为 TCP 全双工,服务端收到 FIN 后往往还有数据要发完,所以 ACK 和 FIN 要分开发。
高频追问:为什么TIME_WAIT要等 2MSL(保证最后的 ACK 能到达 + 让旧连接报文自然消亡);什么是SYN Flood 攻击、SYN Cookie 怎么防。
61. WebSocket
WebSocket 提供浏览器与服务器的全双工、长连接通信,解决 HTTP 请求-响应模式无法"服务器主动推送"的问题(传统靠轮询/长轮询,浪费资源且有延迟)。
建立过程:借用 HTTP 协议完成握手——客户端发 Upgrade: websocket 的 GET 请求,服务器返回 101 后协议升级为 WebSocket,之后双方随意双向收发帧(文本/二进制),无需再带 HTTP 头。
特性与对比:
- 全双工、低延迟(首包小)、省头开销;
- 有连接状态;断线需重连机制(心跳 ping/pong);
- 与 SSE 对比:SSE 是单向(服务器→客户端)、基于 HTTP、自动重连、只能文本;WebSocket 双向、可二进制。
前端应用:实时聊天、协同编辑、行情推送、在线游戏、直播弹幕。
注意点:连接管理(close 清理)、心跳保活、重连退避、消息协议设计(JSON 帧 + 类型字段)、wss 加密、与网关/负载均衡兼容。
62. UDP
UDP(用户数据报协议):无连接、不可靠、面向数据报的传输协议。
特点:无三次握手(低延迟)、无确认重传(丢包不重发)、无拥塞控制、每个数据报独立(有长度边界)、支持广播多播、头部仅 8 字节。
适用场景:实时性 > 可靠性:视频/语音通话(可丢帧不可卡)、游戏、DNS 查询、DHCP、直播;QUIC(HTTP/3 传输层)也基于 UDP(在用户态实现可靠机制)。
对比考点(TCP vs UDP):
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接 | 无连接 |
| 可靠性 | 可靠(确认重传) | 不可靠(尽力而为) |
| 速度/开销 | 慢/头大 | 快/头小 |
| 用途 | 文件/网页/邮件 | 音视频/游戏/DNS |
63. 七层网络模型(OSI)
OSI 七层(自下而上)记忆:物数网传会表应。
| 层 | 职责 | 典型协议/设备 |
|---|---|---|
| 7 应用层 | 为用户应用提供网络服务 | HTTP/HTTPS、DNS、FTP、SMTP |
| 6 表示层 | 数据格式、加密、压缩 | 编解码(TLS 常归此层概念) |
| 5 会话层 | 建立/管理/终止会话 | 会话管理(常与 4/7 合并理解) |
| 4 传输层 | 端到端可靠传输、端口 | TCP、UDP |
| 3 网络层 | 路由寻址、IP | IP、ICMP;路由器 |
| 2 数据链路层 | 相邻节点帧传输、MAC | 以太网、ARP;交换机 |
| 1 物理层 | 比特流、物理信号 | 网线、集线器 |
工程上常讲 TCP/IP 四层:应用层(含 OSI 5-7)、传输层(4)、网络层(3)、网络接口层(1-2)。
数据封装:发送方自上而下加头(应用数据 → TCP 头 → IP 头 → 帧头),接收方反向解封装。
考点:能默写各层 + 说清"HTTP 在哪层、TCP/UDP 在哪层、IP 在哪层";DNS 用 UDP(53,也可 TCP);HTTPS 的 TLS 握手在传输层之上。
64. UglifyJS 压缩原理
UglifyJS(及 terser/压缩器)对 JS 做三步:解析(parse)→ 转换/优化 → 生成(generate)。
核心压缩手段:
- AST 解析:把源码解析成抽象语法树,后续所有操作在 AST 上进行;
- 作用域分析 + 变量重命名:把局部变量/函数名替换为极短名(a、b、c),全局保留(可配置 mangle);
- 死代码删除(DCE):删除
if(false)、不可达分支、未被使用的变量/函数; - 常量折叠与求值:
1+2→3,"ab"+"cd"→"abcd",布尔表达式化简; - 语句合并/去括号:压缩空白换行、合并 var 声明、缩短条件表达式;
- 代码生成:输出最紧凑形式。
原理要点:本质是"解析成 AST → 变换(重命名/删枝/常量折叠)→ 重新序列化",与 babel/webpack 共享同一"编译器三阶段"心智模型。
考察点:说出"mangle(改名)与 compress(优化)两个阶段";为何不能用正则做压缩(需理解作用域);压缩后源码不可读 → 生产调试用 sourcemap。
65. Babel 原理
Babel 是 JS 编译器:把新语法/新特性转成目标环境可运行的代码。三段式:Parse → Transform → Generate。
- Parse 解析:源码 → 词法分析(tokenize)→ 语法分析 → AST;
- Transform 转换:遍历 AST(visitor 访问者模式),按插件规则改写节点(如把箭头函数转为普通函数);@babel/preset-env 根据 browserslist 目标自动启用所需插件;
- Generate 生成:把转换后的 AST 输出为新代码 + sourcemap。
关键概念:
- plugin 与 preset 顺序:plugin 先于 preset 执行,plugin 内部从前往后、preset 从后往前;
- 只能转语法(arrow、class、可选链),新 API(Promise、Array.includes)靠 polyfill:core-js(
useBuiltIns:'usage'按需引入)+ @babel/runtime(辅助函数复用,避免重复注入); - helper 函数(_extends、_classCallCheck)由 runtime 注入。
考察点:能画出 Parse/Transform/Generate 流程;说清"语法 vs API(polyfill)"两个维度;理解为什么 TS 编译也走类似管线。
66. Webpack 工作流程
Webpack 是模块打包器:把各种资源(JS/CSS/图片)当作模块,按依赖图打包成浏览器可用文件。
核心流程:
- 初始化:读取配置、创建 Compiler、注册插件(插件的 apply 钩子全部注册);
- 解析入口:从 entry 出发,生成模块依赖图;
- 对每个文件:调用 loader 把非 JS 资源转为 JS(如 .vue、.css);
- 用 acorn 等解析成 AST,分析 import/require,收集依赖,递归处理(有缓存);
- 构建 chunk:按入口与 splitChunks 策略把模块分组为 chunk;
- 生成产物:为每个 chunk 生成 bundle 文件(用模板包裹模块,实现 require/模块注册运行时);
- 输出:写文件 + 执行插件(如 HTML 插件注入脚本、压缩、sourcemap)。
Tapable 事件流:webpack 本质是事件驱动架构,Compiler/Compilation 在关键节点广播钩子(beforeRun、compile、make、emit、done…),插件订阅钩子介入。
考察点:loader 在"模块解析"阶段转换内容;plugin 在整个生命周期挂钩子;能结合项目讲优化(构建提速:缓存、多进程;产物优化:分包、tree shaking)。
67. Webpack 插件机制
Plugin(插件) 在 webpack 生命周期的钩子上介入,能做 loader 做不了的事(打包产物处理、环境变量注入、多页生成、打包优化、分析报告)。
原理:webpack 内置 Tapable 事件流系统。Compiler 与 Compilation 上挂大量钩子(hooks),插件本质是"一个有 apply(compiler) 方法的对象",在 apply 里用 compiler.hooks.xxx.tap('Name', callback) 订阅钩子,在对应时机执行回调、拿到 compilation/assets 等上下文修改。
// 一个最简单的插件:打包结束打印统计
class MyPlugin {
apply(compiler) {
compiler.hooks.done.tap('MyPlugin', (stats) => {
console.log('构建完成', stats.hasErrors() ? '有错误' : 'OK');
});
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, cb) => {
compilation.assets['version.json'] = {
source: () => JSON.stringify({ v: Date.now() }),
size: () => 0,
};
cb();
});
}
}
常用插件举例:HtmlWebpackPlugin(生成 HTML 注入 bundle)、MiniCssExtractPlugin(抽 CSS)、TerserPlugin(压缩)、BundleAnalyzerPlugin(体积分析)、DefinePlugin(注入全局常量)。
考点:Plugin vs Loader 职责边界;钩子(hooks)模型与 Tapable;写过一个自定义插件是加分项。
68. Webpack Loader 机制
Loader:把非 JS 资源转换成 webpack 能处理的模块(本质是导出函数的转换器:输入源码/内容 → 输出新内容)。loader 在模块解析阶段执行。
- 配置在
module.rules,按 test(文件匹配)+ use(loader 列表); - 多个 loader 组成流水线,执行顺序从右到左 / 从下到上(
use:['a','b','c']先执行 c 再到 a); - loader 可带 options;
- pitch 阶段(进阶):loader 有 pitch 方法,可在读取模块前拦截/熔断 loader 链。
// 自定义 loader 示例:把源码中的 TODO 注释替换
module.exports = function (source) {
return source.replace(/\/\/\s*TODO[^\n]*/g, '// 已处理');
};
常见 loader:babel-loader(ES6+ → ES5)、css-loader + style-loader(或 MiniCssExtractPlugin.loader)、sass/less-loader、vue-loader(解析 .vue)、ts-loader / esbuild-loader、url-loader/file-loader(图片字体)。
考点:loader 是"资源转换器"、plugin 是"生命周期介入者";执行顺序;"loader 单一职责、可用多个";对比 vite 的 esbuild/原生 ESM 预构建思路。
69. 前端微服务(微前端)
微前端:把大型前端应用拆成多个可独立开发、独立部署、技术栈无关的子应用,由一个主应用(基座)统一加载与组织。
核心问题与方案:
- 应用加载(路由分发):主应用按路由加载对应子应用。
- single-spa(生命周期管理);qiankun(基于 single-spa + HTML Entry,开箱即用);iframe(隔离最强但通信/体验差);module federation(webpack5 模块联邦)——运行时共享模块。
- 样式隔离:CSS Modules/scoped、容器前缀、shadow DOM。
- JS 沙箱(核心难点):子应用全局污染主应用。
- qiankun 用 Proxy 沙箱:对 window 打快照/代理读写,卸载时还原;
- 子应用生命周期:bootstrap/mount/unmount 由主应用调度(卸载须清理全局副作用、监听器)。
- 通信:自定义事件、全局状态(qiankun 的 setGlobalState/onGlobalStateChange)、postMessage。
- 公共依赖:抽公共库 + externals + 全局变量,或 module federation 共享。
考察点:拆分价值与引入成本;对比 iframe/qiankun/module federation;子应用为什么"卸载必须清理全局"。微前端适合中大型企业应用,小项目别硬上。
70. 算法:反转链表
链表节点原地反转(迭代 + 递归两种):
function ListNode(v, next = null) { this.val = v; this.next = next; }
// 迭代:prev/curr 指针逐个反转
function reverseList(head) {
let prev = null, curr = head;
while (curr) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
// 递归
function reverseListRec(head) {
if (!head || !head.next) return head;
const newHead = reverseListRec(head.next);
head.next.next = head; // 让下一个节点指回自己
head.next = null;
return newHead;
}
进阶:反转前 N 个、区间反转(m-n)、K 个一组反转、回文链表判断(快慢指针+反转后半)。
考点:画图理解指针操作;边界(空/单节点);空间 O(1)。
71. 算法:链表有环
快慢指针(Floyd 判圈):慢指针一步、快指针两步,若有环必相遇;再让慢指针从头、快指针从相遇点各走一步,相遇点即环入口。
function hasCycle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}
// 找环入口:meet 点后,head 与 meet 同速走,交点即入口
function detectCycle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next; fast = fast.next.next;
if (slow === fast) {
let p = head;
while (p !== slow) { p = p.next; slow = slow.next; }
return p;
}
}
return null;
}
证明要点:快慢相遇时,慢走 k,快走 2k,差 k 为环长整数倍;从头出发与相遇点同速走会在环入口汇合。
扩展:用 Set 记录访问过的节点(O(n) 空间)作为弱化版;判断两链表是否相交(双指针法)。
72. 算法:判断括号字符串是否有效
经典栈应用:遍历字符,左括号入栈,右括号时与栈顶匹配则弹出,否则失败;最后栈空即有效。
function isValid(s) {
const map = { ')': '(', ']': '[', '}': '{' };
const stack = [];
for (const ch of s) {
if (ch in map) { // 右括号
if (stack.pop() !== map[ch]) return false;
} else {
stack.push(ch); // 左括号入栈
}
}
return stack.length === 0;
}
isValid('()[]{}'); // true
isValid('([)]'); // false
考察点:栈的"就近匹配"特性;变体:括号加通配符、括号内求值、最长有效括号。
73. 算法:返回数组中第 k 个最大元素
方案与复杂度:
- 排序:
nums.sort((a,b)=>b-a)[k-1]——O(n log n),最简单; - 快排分区(QuickSelect):每次 partition 把枢纽放到正确位置,根据其下标只递归一侧——平均 O(n),最坏 O(n²);随机选枢纽规避最坏;
- 小顶堆维护大小为 k:堆超 k 弹出最小,堆顶即第 k 大——O(n log k),适合数据流(Top K);
- 语言内置:Python
heapq.nlargest。
// 快速选择(随机化枢轴)
function findKthLargest(nums, k) {
const target = nums.length - k; // 第 k 大 = 升序第 n-k 位
let lo = 0, hi = nums.length - 1;
while (lo <= hi) {
const pivotIdx = partition(nums, lo, hi);
if (pivotIdx === target) return nums[pivotIdx];
pivotIdx < target ? (lo = pivotIdx + 1) : (hi = pivotIdx - 1);
}
}
// Lomuto 分区(随机选枢轴后交换到 hi 位,规避有序数组的最坏情况)
function partition(nums, lo, hi) {
const rand = lo + Math.floor(Math.random() * (hi - lo + 1));
[nums[rand], nums[hi]] = [nums[hi], nums[rand]]; // 枢轴换到末尾
const pivot = nums[hi];
let i = lo; // i 指向"小于枢轴区"的下一位
for (let j = lo; j < hi; j++) {
if (nums[j] < pivot) { [nums[i], nums[j]] = [nums[j], nums[i]]; i++; }
}
[nums[i], nums[hi]] = [nums[hi], nums[i]]; // 枢轴归位
return i; // 返回枢轴最终下标
}
考点:三种方案及复杂度;"第 k 大是 Top-K/大数据入口"(海量数据用堆)。注意:QuickSelect 的 partition 常被面试官要求手写,别只写主函数——这是本题最容易掉分的地方。
74. 算法:找出数组中和为 sum 的 n 个数
求数组中 n 个数之和等于目标值:
- 两数之和:哈希表一次遍历 O(n)——
map[target - num]命中即返回; - 三数之和:排序 + 定第一个 + 双指针扫剩余,注意去重(跳过相同值);
- k-sum 通用:排序后递归降维(每层固定一个数),剪枝。
// 三数之和=0(去重版)
function threeSum(nums) {
nums.sort((a, b) => a - b);
const res = [];
for (let i = 0; i < nums.length - 2; i++) {
if (nums[i] > 0) break;
if (i > 0 && nums[i] === nums[i-1]) continue; // 去重
let l = i + 1, r = nums.length - 1;
while (l < r) {
const s = nums[i] + nums[l] + nums[r];
if (s === 0) {
res.push([nums[i], nums[l], nums[r]]);
while (l < r && nums[l] === nums[l+1]) l++; // 去重
while (l < r && nums[r] === nums[r-1]) r--;
l++; r--;
} else s < 0 ? l++ : r--;
}
}
return res;
}
考点:两数→三数的升级思路;双指针依赖排序;去重逻辑是否完整。
75. 算法(贪心):具有给定数值的最小字符串
给定 n(长度)与 k(字符数值和,a=1…z=26),构造字典序最小的字符串。
思路:贪心——先全放 'a',从右往左把多余的 k 尽量加给低位(靠后位置加大对字典序影响小)。
function getSmallestString(n, k) {
const arr = new Array(n).fill(97); // 'a'
let rest = k - n; // 全 a 后多出的和
for (let i = n - 1; i >= 0 && rest > 0; i--) {
const add = Math.min(25, rest); // 最多再加到 'z'
arr[i] += add;
rest -= add;
}
return String.fromCharCode(...arr);
}
// n=3,k=27 → 'aaz'(字典序最小)
考点:贪心选择 + 可行性剪枝(剩余位能否承担和);从低位补满的思想。(LeetCode 1663)
76. 算法:二叉树最大深度
DFS 递归(最简)或 BFS 层数:
// 递归 DFS
function maxDepth(root) {
if (!root) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
// BFS 迭代(每层 +1)
function maxDepthBfs(root) {
if (!root) return 0;
let depth = 0, q = [root];
while (q.length) {
depth++;
const size = q.length;
for (let i = 0; i < size; i++) {
const n = q.shift();
if (n.left) q.push(n.left);
if (n.right) q.push(n.right);
}
}
return depth;
}
考点:递归树形遍历;与最小深度对比(注意单子树特判);迭代解法。
77. 算法:二叉树层次遍历
BFS 按层输出(每层一个数组):
function levelOrder(root) {
if (!root) return [];
const res = [], q = [root];
while (q.length) {
const size = q.length, level = [];
for (let i = 0; i < size; i++) {
const n = q.shift();
level.push(n.val);
if (n.left) q.push(n.left);
if (n.right) q.push(n.right);
}
res.push(level);
}
return res;
}
变体考点:锯齿形(奇偶层反转 level);自底向上层序(最后 reverse);右侧视图(取每层最后节点);每层最大值。
考点:用 size 快照实现"按层";空间 O(宽度)。
78. 算法(剪枝):判断数独是否有效
只需验证已填数字是否冲突:行、列、3×3 宫各用 Set 去重。
function isValidSudoku(board) {
const rows = new Array(9).fill(0).map(() => new Set());
const cols = new Array(9).fill(0).map(() => new Set());
const boxes = new Array(9).fill(0).map(() => new Set());
for (let i = 0; i < 9; i++)
for (let j = 0; j < 9; j++) {
const v = board[i][j];
if (v === '.') continue;
const b = Math.floor(i / 3) * 3 + Math.floor(j / 3); // 宫号
if (rows[i].has(v) || cols[j].has(v) || boxes[b].has(v)) return false;
rows[i].add(v); cols[j].add(v); boxes[b].add(v);
}
return true;
}
进阶:求解数独 = 回溯(DFS + 剪枝 + 候选数优化)。
考点:宫号计算 (i/3)*3 + j/3;只用 3 个 Set 数组的空间技巧;延伸回溯。
79. 算法(二分查找):求解平方根
求 sqrt(x) 的整数部分。整数二分 + 边界处理:
function mySqrt(x) {
if (x < 2) return x;
let lo = 1, hi = Math.floor(x / 2);
while (lo <= hi) {
const mid = (lo + hi) >> 1;
const sq = mid * mid;
if (sq === x) return mid;
sq < x ? (lo = mid + 1) : (hi = mid - 1);
}
return hi; // 返回不大于 sqrt 的最大整数
}
// 浮点精度版:二分边界用左右误差 <1e-7
考点:二分搜索"最后一个满足 mid²≤x"的位置;溢出用 mid <= x/mid 比较避免乘法溢出。
80. 算法(字典树):实现一个字典树(Trie)
字典树用于字符串前缀匹配/词频统计。节点含子节点表 + 结束标记:
class Trie {
constructor() { this.children = {}; this.isEnd = false; }
insert(word) {
let node = this;
for (const ch of word) {
node.children[ch] ||= new Trie();
node = node.children[ch];
}
node.isEnd = true;
}
search(word) {
const node = this._find(word);
return !!node && node.isEnd;
}
startsWith(prefix) {
return !!this._find(prefix);
}
_find(str) {
let node = this;
for (const ch of str) {
if (!node.children[ch]) return null;
node = node.children[ch];
}
return node;
}
}
考点:插入/查找 O(词长);应用场景(自动补全、敏感词过滤、IP 路由最长匹配);空间用哈希表子节点;进阶:压缩字典树、AC 自动机。
82. 算法:最短距离
典型的网格/图最短路径题,分场景选算法:
- 无权图/单位权(网格四方向步数)→ BFS:首次到达即最短;可双向 BFS 剪枝;
- 带权且非负 → Dijkstra(优先队列/最小堆):每次取距离最小的点松弛邻居,O(E log V);
- 带负权 → Bellman-Ford / SPFA(能检测负环);
- 所有点对 → Floyd(O(V³));
- 网格里若只有 0/1 障碍可用 0-1 BFS(双端队列)。
// 模板:二维网格 BFS 最短步数(0 可走 1 障碍)
function shortestPath(grid, sr, sc, tr, tc) {
const m = grid.length, n = grid[0].length;
const dist = Array.from({length:m}, () => Array(n).fill(Infinity));
const q = [[sr, sc]];
dist[sr][sc] = 0;
const dirs = [[1,0],[-1,0],[0,1],[0,-1]];
while (q.length) {
const [r, c] = q.shift();
if (r === tr && c === tc) return dist[r][c];
for (const [dr, dc] of dirs) {
const nr = r+dr, nc = c+dc;
if (nr<0||nc<0||nr>=m||nc>=n||grid[nr][nc]===1) continue;
if (dist[nr][nc] > dist[r][c]+1) {
dist[nr][nc] = dist[r][c]+1;
q.push([nr, nc]);
}
}
}
return -1;
}
考点:先问清"有无权重/方向限制"再选 BFS 还是 Dijkstra。
83. 算法:LRU 缓存
LRU(最近最少使用)淘汰策略:容量满时淘汰最久未访问的键。要求 get/put 都 O(1)。
数据结构:哈希表(O(1) 定位)+ 双向链表(O(1) 增删/移动头尾)。链表头=最近使用,尾=最久未用。
// 基于 Map 插入序的简化实现
class LRUCache {
constructor(capacity) { this.cap = capacity; this.map = new Map(); }
get(key) {
if (!this.map.has(key)) return -1;
const v = this.map.get(key);
this.map.delete(key);
this.map.set(key, v); // 删了重插 → 移到末尾=最近使用
return v;
}
put(key, value) {
if (this.map.has(key)) this.map.delete(key);
this.map.set(key, value);
if (this.map.size > this.cap) {
this.map.delete(this.map.keys().next().value); // 最久未用 = 开头
}
}
}
面试标准实现(考察真原理):
class LRU { // 手写双向链表版:head(新)->...->tail(旧)
constructor(cap){ this.cap=cap; this.cache=new Map(); this.head={}; this.tail={};
this.head.next=this.tail; this.tail.prev=this.head; }
_remove(n){ n.prev.next=n.next; n.next.prev=n.prev; }
_addFront(n){ n.next=this.head.next; n.prev=this.head; this.head.next.prev=n; this.head.next=n; }
get(k){ const n=this.cache.get(k); if(!n) return -1; this._remove(n); this._addFront(n); return n.val; }
put(k,v){ let n=this.cache.get(k); if(n){ n.val=v; this._remove(n); this._addFront(n); return; }
n={k,val:v}; this.cache.set(k,n); this._addFront(n);
if(this.cache.size>this.cap){ const old=this.tail.prev; this._remove(old); this.cache.delete(old.k); } }
}
考点:为什么必须"哈希+链表";Map 实现可作快速方案但要说清原理;变体 LFU。