← 返回题库
前端核心·真题演练

前端真题 130 题 · 性能网络工程化算法

缓存策略、白屏、RAIL、Tree Shaking、重排重绘、浏览器架构、HTTP/2/3、TCP/UDP、Webpack、微前端、算法题等 49-79 题。

性能网络HTTPWebpack算法微前端

49. 缓存策略

HTTP 缓存分强缓存协商缓存两级:

① 强缓存(不发请求,直接用本地)

  • Cache-Control(HTTP/1.1 优先):max-age=3600public/privateno-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 前)的优化问题。白屏成因 + 对策:

成因

  1. CSS/JS 阻塞<script> 在 head 同步执行阻塞 DOM 解析;CSS 未加载完阻塞渲染;
  2. 首屏体积过大:JS 包太大,下载+解析执行时间长 → 首屏迟迟不渲染;
  3. 字体加载阻塞(FOIT);接口依赖首屏(组件等数据才渲染);
  4. 框架初始化重、错误抛错导致整树不渲染。

对策

  • 关键 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';

实践注意

  1. 必须是 ESM 源码(import/export),CommonJS require 无法静态分析 → 库要同时发 ESM 包(package.json module 字段 / exports);
  2. sideEffects 声明:CSS/全局样式等有副作用的文件要在 sideEffects 白名单,否则被误删;
  3. babel/tsconfig 别把模块转成 CJS(preset-env 的 modules:false);
  4. 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(不改变布局)。

优化

  1. 批量修改 DOM(DocumentFragment、display:none 后改再显示、className 切换);
  2. 缓存布局属性读取(避免读写交替的 layout thrashing);
  3. transform/opacity 动画(合成层,完全不触发重排重绘);
  4. 减小 DOM 规模与层级;
  5. 必要时 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: fixedvideo/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.1 是文本协议,2.0 把消息切成二进制帧(HEADERS/DATA),更高效解析;
  2. 多路复用(Multiplexing):一个 TCP 连接上并发多个请求/响应流(stream),互不阻塞——解决 1.1 的队头阻塞(每连接串行、浏览器约 6 连接上限);
  3. 头部压缩(HPACK):用静态表 + 动态表 + Huffman 压缩请求头,减少重复头传输;
  4. 服务器推送(Server Push):服务器可主动推资源——因实现复杂、缓存控制难,Chrome 已移除此特性,勿重点吹;
  5. 优先级与流控制:可设资源加载优先级。

仍存在的问题:TCP 层面的队头阻塞(丢包时整个连接等待重传);需要 TLS(实际部署 2.0 基本都加密)。

考察点:1.1 vs 2.0 对比表;"为什么还要 3.0"的引出;多路复用如何解决连接数瓶颈。


59. HTTP/3.0

核心:把传输层从 TCP 换成 QUIC(基于 UDP),解决 HTTP/2 的 TCP 队头阻塞。

改进点

  1. 基于 UDP + QUIC:QUIC 在用户态实现可靠传输、拥塞控制、TLS1.3 集成(0-RTT/1-RTT 握手,建连快);
  2. 无队头阻塞:TCP 丢一个包会阻塞后续所有流;QUIC 每个流独立,丢包只影响该流;
  3. 连接迁移:连接用 Connection ID 标识,切换网络(WiFi→4G)不断连;
  4. 0-RTT 恢复:复用缓存密钥时首包即可带数据,弱网/移动端体验提升;
  5. 内建前向纠错、加密默认(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(传输控制协议):面向连接、可靠、字节流传输的协议。三要素:三次握手建立、四次挥手断开、确认重传保可靠。

三次握手(建立连接,双方确认收发能力):

  1. 客户端发 SYN(seq=x);
  2. 服务端回 SYN+ACK(seq=y, ack=x+1);
  3. 客户端发 ACK(ack=y+1)——此后可传数据。

为什么三次:两次无法确认客户端已收到服务端能力(防止已失效连接请求突然到达造成资源浪费/错误建立)。

四次挥手(断开连接,因全双工需各自关闭):

  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):

维度TCPUDP
连接面向连接无连接
可靠性可靠(确认重传)不可靠(尽力而为)
速度/开销慢/头大快/头小
用途文件/网页/邮件音视频/游戏/DNS

63. 七层网络模型(OSI)

OSI 七层(自下而上)记忆:物数网传会表应

职责典型协议/设备
7 应用层为用户应用提供网络服务HTTP/HTTPS、DNS、FTP、SMTP
6 表示层数据格式、加密、压缩编解码(TLS 常归此层概念)
5 会话层建立/管理/终止会话会话管理(常与 4/7 合并理解)
4 传输层端到端可靠传输、端口TCP、UDP
3 网络层路由寻址、IPIP、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)

核心压缩手段:

  1. AST 解析:把源码解析成抽象语法树,后续所有操作在 AST 上进行;
  2. 作用域分析 + 变量重命名:把局部变量/函数名替换为极短名(a、b、c),全局保留(可配置 mangle);
  3. 死代码删除(DCE):删除 if(false)、不可达分支、未被使用的变量/函数;
  4. 常量折叠与求值1+23"ab"+"cd""abcd",布尔表达式化简;
  5. 语句合并/去括号:压缩空白换行、合并 var 声明、缩短条件表达式;
  6. 代码生成:输出最紧凑形式。

原理要点:本质是"解析成 AST → 变换(重命名/删枝/常量折叠)→ 重新序列化",与 babel/webpack 共享同一"编译器三阶段"心智模型。

考察点:说出"mangle(改名)与 compress(优化)两个阶段";为何不能用正则做压缩(需理解作用域);压缩后源码不可读 → 生产调试用 sourcemap。


65. Babel 原理

Babel 是 JS 编译器:把新语法/新特性转成目标环境可运行的代码。三段式:Parse → Transform → Generate

  1. Parse 解析:源码 → 词法分析(tokenize)→ 语法分析 → AST
  2. Transform 转换:遍历 AST(visitor 访问者模式),按插件规则改写节点(如把箭头函数转为普通函数);@babel/preset-env 根据 browserslist 目标自动启用所需插件;
  3. Generate 生成:把转换后的 AST 输出为新代码 + sourcemap。

关键概念

  • plugin 与 preset 顺序:plugin 先于 preset 执行,plugin 内部从前往后、preset 从后往前;
  • 只能转语法(arrow、class、可选链),新 API(Promise、Array.includes)靠 polyfillcore-jsuseBuiltIns:'usage' 按需引入)+ @babel/runtime(辅助函数复用,避免重复注入);
  • helper 函数(_extends、_classCallCheck)由 runtime 注入。

考察点:能画出 Parse/Transform/Generate 流程;说清"语法 vs API(polyfill)"两个维度;理解为什么 TS 编译也走类似管线。


66. Webpack 工作流程

Webpack 是模块打包器:把各种资源(JS/CSS/图片)当作模块,按依赖图打包成浏览器可用文件。

核心流程

  1. 初始化:读取配置、创建 Compiler、注册插件(插件的 apply 钩子全部注册);
  2. 解析入口:从 entry 出发,生成模块依赖图
    • 对每个文件:调用 loader 把非 JS 资源转为 JS(如 .vue、.css);
    • 用 acorn 等解析成 AST,分析 import/require,收集依赖,递归处理(有缓存);
  3. 构建 chunk:按入口与 splitChunks 策略把模块分组为 chunk;
  4. 生成产物:为每个 chunk 生成 bundle 文件(用模板包裹模块,实现 require/模块注册运行时);
  5. 输出:写文件 + 执行插件(如 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. 前端微服务(微前端)

微前端:把大型前端应用拆成多个可独立开发、独立部署、技术栈无关的子应用,由一个主应用(基座)统一加载与组织。

核心问题与方案

  1. 应用加载(路由分发):主应用按路由加载对应子应用。
    • single-spa(生命周期管理);qiankun(基于 single-spa + HTML Entry,开箱即用);iframe(隔离最强但通信/体验差);module federation(webpack5 模块联邦)——运行时共享模块。
  2. 样式隔离:CSS Modules/scoped、容器前缀、shadow DOM。
  3. JS 沙箱(核心难点):子应用全局污染主应用。
    • qiankun 用 Proxy 沙箱:对 window 打快照/代理读写,卸载时还原;
    • 子应用生命周期:bootstrap/mount/unmount 由主应用调度(卸载须清理全局副作用、监听器)。
  4. 通信:自定义事件、全局状态(qiankun 的 setGlobalState/onGlobalStateChange)、postMessage。
  5. 公共依赖:抽公共库 + 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 个最大元素

方案与复杂度

  1. 排序nums.sort((a,b)=>b-a)[k-1]——O(n log n),最简单;
  2. 快排分区(QuickSelect):每次 partition 把枢纽放到正确位置,根据其下标只递归一侧——平均 O(n),最坏 O(n²);随机选枢纽规避最坏;
  3. 小顶堆维护大小为 k:堆超 k 弹出最小,堆顶即第 k 大——O(n log k),适合数据流(Top K);
  4. 语言内置: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. 算法:最短距离

典型的网格/图最短路径题,分场景选算法:

  1. 无权图/单位权(网格四方向步数)→ BFS:首次到达即最短;可双向 BFS 剪枝;
  2. 带权且非负 → Dijkstra(优先队列/最小堆):每次取距离最小的点松弛邻居,O(E log V);
  3. 带负权 → Bellman-Ford / SPFA(能检测负环);
  4. 所有点对 → Floyd(O(V³));
  5. 网格里若只有 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。