大型游戏官网与活动页面 · 简历关键词速查

网易(杭州)网络有限公司 · 前端工程师 | C 端高性能 · 2018.07 – 2019.12
涉及《倩女幽魂》《遇见逆水寒》《天谕》等多个高流量游戏,日 PV 10w+
简历原文:利用一整套性能优化手段,包括首屏加载优化路由和组件懒加载动画压缩缓存管理Webpack 打包优化等,关注 LCP/FID/CLS 性能指标,把复杂移动端页面加载时间(LCP)从 1.5s 降低到 500ms。

1 首屏加载优化

注水屏 vs 骨架屏

  • 骨架屏(Skeleton):页面未加载完时展示占位 UI,减少白屏感知,纯视觉优化
    实现方式
    • Webpack 插件(如 page-skeleton-webpack-plugin):构建时用 Puppeteer 打开页面,截取 DOM 轮廓自动生成骨架屏 HTML,注入到模板中
  • 注水屏:页面加载期间展示网易 Logo 的注水填充动画,给用户明确的加载进度感知,比白屏或静态 loading 体验更好
    实现方式
    • Logo 轮廓用 SVG path 绘制,内部用 clip-path 裁剪出形状
    • CSS @keyframes 控制一个色块从底部向上填充,配合 wave 波浪效果模拟注水
    • JS 监听资源加载进度(或用定时器模拟),同步驱动填充高度,加载完成后淡出过渡到真实页面
  • 区别:骨架屏模拟页面布局结构做占位;注水屏是品牌化的加载动画,两者都是减少白屏感知的视觉过渡方案

其他首屏优化手段

  • 代码分割:按路由/组件拆分 chunk,首屏只加载必要代码
  • Preload 关键资源:提前告知浏览器加载大图、核心样式、关键字体,缩短关键路径
    实现方式
    • 在 <head> 中加 <link rel="preload" as="image" href="/images/hero.jpg">
    • 字体:<link rel="preload" as="font" href="/fonts/main.woff2" type="font/woff2" crossorigin>
    • CSS:<link rel="preload" as="style" href="/css/critical.css">
    • 只 preload 首屏关键资源(1-2 张大图),不要滥用,否则反而抢占其他资源带宽
  • 图片优化:懒加载 + WebP/AVIF 格式 + 响应式图片(srcset)
可追问点

SSR / SSG / ISR(延伸)

  • SSR:每次请求服务端生成 HTML,首屏快,利于 SEO,但服务器压力大
  • SSG:构建时生成 HTML,最快,适合内容不常变的页面
  • ISR:SSG + 定时/按需重新生成,兼顾性能和时效性

2 路由和组件懒加载

路由级别懒加载

  • 原理:React.lazy + Suspense,底层是动态 import(),Webpack 遇到后自动拆成独立 chunk
    实现方式
    • const Home = React.lazy(() => import('./pages/Home'))
    • 路由配置中用 <Suspense fallback={<Loading />}><Home /></Suspense> 包裹
    • Webpack 遇到动态 import() 会自动将目标模块拆成独立 chunk,运行时按需拉取
    • 可用 webpackChunkName 注释自定义 chunk 名:import(/* webpackChunkName: "home" */ './pages/Home')
  • 效果:用户访问某路由时才下载对应 JS,首屏加载量大幅减少
  • 注意:Suspense fallback 要设计好加载态,避免闪烁

组件级别懒加载

  • IntersectionObserver:检测元素是否进入视口,进入后再加载组件/图片
    实现方式
    • 创建 observer 实例,配置 rootMarginthreshold
    • const observer = new IntersectionObserver(callback, { rootMargin: '200px', threshold: 0 })
    • 对目标元素调用 observer.observe(el),进入视口时触发回调加载真实内容
    • 加载完成后调用 observer.unobserve(el) 停止监听,避免重复触发
    • 优势:不靠频繁监听 scroll 事件,由浏览器原生判断,性能开销更低
可追问点

不定高度、大数据量场景 → 详见侧边栏 07 虚拟滚动与大数据列表

3 动画压缩

动画选型 → 详见侧边栏 08 动画选型

动画压缩与优化

  • 序列帧动画(精灵图):设计输出 JSON 或雪碧图,比 GIF 体积小很多
    序列帧抖动问题及解决方案
    • 抖动原因:亚像素渲染
      • 什么是亚像素:屏幕的最小显示单位是 1 个物理像素,但 CSS 计算出的位置可能是小数,比如 background-position: -133.33px,这就是"亚像素"(sub-pixel)——落在两个物理像素之间
      • 浏览器怎么处理:浏览器必须把小数位置"对齐"到整数像素才能渲染。不同浏览器/设备的对齐策略不同:有的四舍五入,有的向下取整,有的做抗锯齿混合
      • 举例:精灵图总宽 1000px,共 3 帧。每帧宽度 = 1000 / 3 = 333.33px。第 1 帧 0px,第 2 帧 -333.33px,第 3 帧 -666.67px。浏览器把 333.33 取整为 333 或 334,每帧实际偏移就差了 1px,连续播放时画面就会左右抖动
      • 移动端更严重:DPR=2 的设备 1 个 CSS 像素 = 2 个物理像素,DPR=3 则是 3 个。CSS 算出的小数经过 DPR 放大后,取整误差也被放大,抖动更明显
    • 项目中的方案:用 SVG 包裹序列帧
      • 将精灵图作为 <image> 放进 SVG 中,通过 SVG 的 <animate> 或 CSS 控制 viewBox / 裁切区域来切换帧
      • SVG 内部坐标系是矢量的、精确的,不存在 CSS background-position 的亚像素取整问题
      • 每一帧的裁切位置由 SVG 坐标精确控制,不会因为浏览器像素对齐策略而产生偏移
    • 为什么 SVG 不抖:
      • CSS background-position 在不同 DPR 下会做像素对齐(snap to pixel),导致每帧偏移不一致
      • SVG 的坐标系统独立于设备像素,位置计算在矢量空间完成后才光栅化,帧切换位移完全精确
      • SVG 天然支持任意缩放不失真,高 DPI 适配也不需要额外处理
    • 对比其他方案:
      • CSS steps(N) — 能解决补间问题,但亚像素偏移在某些设备上仍存在
      • JS 逐帧切换 <img> src — 可行但请求多、内存开销大
      • SVG 包裹 — 精度最高、兼容性好、无额外请求,是项目最终采用的方案
  • 图片压缩:可以用 TinyPNG 等工具对精灵图和其他图片资源做压缩
    TinyPNG 压缩原理
    • 有损量化:将 PNG 24 位真彩色(1600 万色)量化为 8 位索引色(256 色),通过智能算法选择最接近原色的调色板,肉眼几乎看不出差异
    • 体积效果:通常能压缩 60%-80%,比如 1MB 的图压到 200-400KB
    • JPEG 压缩:TinyPNG 也支持 JPEG,通过降低色度采样精度和优化霍夫曼编码表来减小体积
  • will-change:提前告知浏览器哪些属性会变,触发合成层优化
  • 减少重排重绘:只动 transform/opacity,避免动 width/height/top/left
  • requestAnimationFrame:替代 setTimeout 做帧动画,保证与屏幕刷新同步

动画组件库封装

  • 封装通用动画库组件,解决移动端动画联动问题
  • 提高复用性和拓展能力,研发速度提升 30%

4 缓存管理

强缓存 vs 协商缓存

  • 强缓存(Cache-Control / Expires):浏览器直接读本地,不发请求。适用于图片、JS、CSS 等静态资源,配合文件名 hash 更新
  • 协商缓存(ETag / Last-Modified):浏览器发请求问服务器,没变返回 304。适用于 HTML、API 数据等可能变化的内容
  • HTML 用哪个?协商缓存。因为 HTML 是入口文件,用强缓存会导致用户看不到最新页面
  • 实际策略:静态资源强缓存 + hash;HTML 协商缓存;两者可结合使用

cookie / localStorage / sessionStorage

  • cookie:4KB,自动携带到服务端,可设 HttpOnly/Secure/SameSite
  • localStorage:5MB,持久存储,关浏览器不丢
  • sessionStorage:5MB,关标签页即清除

常见追问

  • cookie 能跨源传递吗?默认不能。
    详细解释
    • 同源 vs 同站:cookie 的限制是按"站点"(site)而不是"源"(origin)。同站 = 协议 + eTLD+1 相同(比如 a.example.comb.example.com 是同站),跨站 = 不同站点之间(比如 example.comother.com
    • SameSite 属性控制跨站行为:
      • SameSite=Strict — 完全不允许跨站携带,最严格。从别的网站点链接过来也不带 cookie
      • SameSite=Lax(浏览器默认值)— 导航级跨站请求(点链接、GET 跳转)会带,但 POST/iframe/AJAX 跨站请求不带
      • SameSite=None — 允许跨站携带,但必须同时设置 Secure(仅 HTTPS 传输)
    • 要实现跨站传 cookie:
      • 服务端设置:Set-Cookie: token=xxx; SameSite=None; Secure
      • 前端 AJAX 请求加:credentials: 'include'(fetch)或 withCredentials: true(axios)
      • 服务端 CORS 响应头:Access-Control-Allow-Credentials: true,且 Access-Control-Allow-Origin 不能是 *,必须指定具体域名
    • 典型场景:第三方登录、嵌入 iframe 的跨站应用、跨域 API 需要带认证信息
  • sessionStorage 跨标签通信?BroadcastChannel,或写入 localStorage 触发 storage 事件
  • 协商缓存典型资源?index.html、API 响应数据、头像图片、低频更新的文档

Preload vs Prefetch

  • preload:当前页面关键资源,高优先级,缩短关键路径(CSS/字体/关键 JS)
  • prefetch:下一页面可能需要的资源,空闲时低优先级加载
  • preload 为什么能优化 LCP?①减少资源发现时间 ②提升关键资源并行加载优先级 ③缩短关键路径总时长

其他缓存机制(了解)

  • Memory Cache:浏览器内存缓存,最快,页面关闭即失效
  • Service Worker:后台脚本拦截请求,支持离线(PWA)。策略:Cache First / Network First
  • IndexedDB:浏览器端 NoSQL,存大量结构化数据
  • CDN 缓存:全球节点分发,就近访问,减少 TTFB
  • HTTP/2 Server Push:服务器主动推送资源,减少往返
  • AppCache:已废弃,被 Service Worker 替代

5 Webpack 打包优化

打包体积优化

  • lodash 按需引入:整包 70KB+,用 babel-plugin-lodash 或 lodash-es 配合 tree shaking 按需引入
  • SplitChunks:抽离公共依赖到单独 chunk,避免多入口重复打包
  • reuseExistingChunk:模块已在某 chunk 中则复用,不生成新 chunk,减少产物体积
    优缺点 & 使用场景
    • 原理:splitChunks 拆分时,如果发现某个模块已经被打进了一个已有 chunk,就直接复用那个 chunk,而不是再生成一个新的。相当于"去重"
    • 优点:减少重复代码,降低总产物体积;chunk 数量更少,浏览器请求数更少
    • 缺点:chunk 之间的依赖关系更隐式,复用的 chunk 可能包含当前路由不需要的代码;修改一个公共模块会导致复用它的所有 chunk 缓存失效
    • 该用的场景(默认开启即可):公共工具库(lodash、dayjs 等)、多页面共享的业务组件、变化频率低的第三方依赖
    • 需要注意的场景:如果某个 chunk 被大量路由复用且体积很大,一次修改会导致大面积缓存失效。此时可以考虑把高频变化的模块单独拆出来,和稳定依赖分开
    • 实际建议:绝大多数项目保持默认 reuseExistingChunk: true 就好,只有在分析 bundle 发现缓存命中率异常时才需要调整
  • Tree Shaking:基于 ESM 静态分析,去除未使用的导出代码

构建速度优化

  • 生产环境(dist):Tree Shaking、Terser 压缩、去除 dead code、scope hoisting
  • 开发环境(dev):HMR 热更新、缩小编译范围(include/exclude)、cache-loader / persistent cache、eval sourcemap
可追问点
Sourcemap 模式详解 — 开发用 eval 系列求快,生产用 hidden 保安全
  • 选择建议:开发用 eval-cheap-module-source-map(快+能看源码);生产用 hidden-source-map(不暴露源码+支持错误监控);不需要调试的生产环境可以直接关掉(false
  • eval:最快构建,不生成 .map 文件,适合开发热更新。把每个模块包在 eval() 里,末尾加 //# sourceURL=...,不做真正映射,报错只能定位到文件
  • cheap-module-source-map:行级映射 + 模块代码,开发调试够用。只映射到行不映射到列,映射的是 loader 处理前的源码(JSX/TS 原文)。"cheap"省列信息提速,"module"保证看到原始源码
  • hidden-source-map:生成 .map 但不在 bundle 中引用,生产环境 + 错误监控上传。bundle 末尾不插入 //# sourceMappingURL,浏览器看不到源码,但 .map 可上传 Sentry 等平台还原堆栈
  • source-map:最完整但最慢。行+列+源码全映射,bundle 含引用。调试体验最好,但构建最慢且暴露源码,一般不在生产使用
Webpack 打包经历哪些过程 — 初始化 → 编译 → 依赖图 → Chunk → Bundle
  • 1. 初始化:读取 webpack.config.js,合并命令行参数,创建 Compiler 对象,加载所有插件(plugin)
  • 2. 编译(Make):从 entry 入口出发,递归解析依赖。每遇到一个 import / require 就找到对应文件,交给匹配的 loader 处理(比如 babel-loader 把 ES6 转 ES5,css-loader 处理样式)
  • 3. 构建模块依赖图:所有文件处理完后,得到一个完整的依赖关系图(Module Graph),知道谁依赖谁
  • 4. 生成 Chunk:根据 entry 和 splitChunks 配置,把 Module 分组成 Chunk。每个 Chunk 是一组将会被打包到一起的模块
  • 5. 输出 Bundle:把每个 Chunk 转换成最终的文件(Bundle),写入磁盘。这一步会执行压缩、添加 runtime 代码等
  • 贯穿全程:Plugin 通过 Tapable 钩子在各阶段介入(比如 HtmlWebpackPlugin 在输出阶段生成 HTML,TerserPlugin 在输出阶段压缩代码)
Module / Chunk / Bundle 区别
概念 是什么 阶段 举例
Module 每一个源文件就是一个 Module,是 Webpack 处理的最小单元 编译阶段 App.jsxutils.jsstyle.css 各是一个 module
Chunk 一组 Module 的集合,是 Webpack 内部的分组单位,决定哪些模块打到一起 打包阶段 entry chunk、vendor chunk、动态 import 产生的 async chunk
Bundle Chunk 经过编译压缩后输出的最终文件,是浏览器真正加载的产物 输出阶段 main.abc123.jsvendor.def456.js
  • 一句话总结:源文件 → Module → 分组成 Chunk → 输出为 Bundle。Module 是输入,Chunk 是中间态,Bundle 是输出
Vite 和 Webpack 区别
对比维度 Webpack Vite
开发启动 先打包所有模块再启动,项目越大越慢 不打包,利用浏览器原生 ESM 按需加载,秒启动
热更新(HMR) 改一个文件可能要重新构建部分依赖链,项目大时变慢 只重新请求改动的模块,速度和项目大小无关
生产构建 自身 bundler,生态成熟,配置灵活 底层用 Rollup(或 Rolldown)打包,产物更干净
底层语言 纯 JS 实现 预构建用 esbuild(Go),速度快 10-100 倍
适用场景 大型老项目、需要高度自定义、复杂微前端 新项目首选、追求开发体验、中小型项目
  • 核心区别一句话:Webpack 是"先打包再开发",Vite 是"先开发再打包"。Vite 开发阶段利用浏览器原生 ESM 跳过打包,所以启动和热更新快得多;生产构建时两者都会打包,差异缩小

6 LCP / FID / CLS

三大指标含义

  • LCP(Largest Contentful Paint):最大内容绘制时间,衡量加载速度。目标 < 2.5s。项目中从 1.5s 降到 500ms
  • FID(First Input Delay):首次输入延迟,衡量交互响应速度。目标 < 100ms
  • CLS(Cumulative Layout Shift):累积布局偏移,衡量视觉稳定性。目标 < 0.1

优化 LCP 的手段(项目重点)

  • Preload 关键资源(CSS、字体、首屏图片)
  • SSR / 骨架屏减少白屏时间
  • 图片压缩 + CDN + 响应式
  • 减少阻塞渲染的 JS(defer / async / 代码分割)

优化 FID

  • 减少主线程长任务(拆分大 JS bundle)
  • 延迟非关键 JS 执行
  • Web Worker 处理密集计算

优化 CLS

  • 图片/视频/广告设置固定宽高或 aspect-ratio
  • 字体加载用 font-display: swap + 预留空间
  • 避免动态注入内容推移已有布局

补充

  • Core Web Vitals 是 Google 搜索排名因素之一,直接影响 SEO
  • 可用 Lighthouse / Chrome DevTools / web-vitals 库 进行度量
可追问点
Chrome DevTools 怎么调试性能指标
  • Performance 面板:
    • 点 Record → 刷新页面 → 停止录制,得到完整的火焰图
    • 看 LCP:时间轴上会标记 LCP 事件,点击可以看到是哪个元素触发的(图片/文字块),以及它在哪个时间点完成渲染
    • 看长任务:Main 线程上红色三角标记的就是长任务(>50ms),这些会阻塞 INP 响应
    • 看 CLS:Experience 行会标记 Layout Shift 事件,点击可以看到是哪个元素发生了偏移、偏移了多少
    • 看帧率:Frames 行显示每帧耗时,绿色是流畅(<16.6ms),红色是掉帧
    • Summary 饼状图:选中时间轴上一段区间后,底部 Summary 标签页会显示开销分布饼图:
      • Loading(蓝色)— 网络请求、HTML 解析
      • Scripting(黄色)— JS 执行,占比高说明 JS 逻辑重,考虑拆分/延后/Web Worker
      • Rendering(紫色)— 样式计算、布局(Layout/Reflow),占比高说明频繁触发重排
      • Painting(绿色)— 绘制像素到屏幕,占比高说明重绘区域大或频繁
      • System(灰色)— 浏览器内部开销(GC、合成等),一般不可控
      • Idle(浅灰)— 空闲时间,占比高说明页面有性能余量
      优化思路:先看饼图哪块最大,再针对性优化。比如 Scripting 大就拆 JS,Rendering 大就减少重排,Painting 大就减少重绘区域
  • Lighthouse 面板:
    • 直接在 DevTools 里跑 Lighthouse 审计,给出 LCP/INP/CLS 等指标评分
    • 每个指标下面有具体优化建议(比如"预加载 LCP 图片""减少未使用的 JS")
    • 注意:Lighthouse 是模拟环境,结果可能和真实用户不同
  • Network 面板辅助:
    • 看首屏关键资源的加载瀑布图,找出哪个资源加载最慢
    • 勾选 Disable cache 模拟首次访问;用 Slow 3G 模拟弱网
    • 看 Size 列区分缓存命中(from cache)和实际请求
前端埋点怎么做 — 统计方案 & 上报方式
  • 性能指标采集:
    • web-vitals 库一行代码采集 LCP/INP/CLS/FCP/TTFB:onLCP(metric => report(metric))
    • 底层是 PerformanceObserver API,监听浏览器原生性能事件
    • 也可以用 performance.getEntriesByType('navigation') 获取页面加载各阶段耗时
  • 上报方式:
    • navigator.sendBeacon(url, data) — 页面卸载时也能可靠发送,不阻塞页面关闭,是埋点上报的首选
    • 备选:fetch + keepalive: true,或 new Image().src(最简单但只能 GET、有长度限制)
    • 批量上报:攒一批数据后统一发送,减少请求数;用 requestIdleCallback 在空闲时上报,不影响用户交互
  • 采样策略:线上不需要 100% 上报,按比例采样(比如 10%)就够统计显著性,减少服务端压力
埋点怎么和业务解耦 — 三种方式:声明式 / Hook封装 / 发布订阅
  • 核心问题:别让业务代码到处散落 track()
  • 方案一:声明式埋点
    • 在元素上加 data-track 这类属性,统一监听点击后自动上报
    • 业务组件不用直接调用埋点 SDK,适合按钮点击、曝光这类通用场景
  • 方案二:封装 Hook / 装饰器
    • 把埋点逻辑放进 useTrack 这种能力里,业务代码只调用封装后的方法,不直接接触埋点实现
    • 比在 onClick 里手写 track() 更干净
  • 方案三:发布订阅模式(解耦最彻底)
    • 业务代码只负责发一个"业务事件",比如"用户点击购买"
    • 埋点系统去订阅这个事件再上报
    • 业务方不知道具体怎么埋点,只知道"发生了什么",解耦的是业务事件和埋点实现之间的关系
口语版:我理解埋点解耦,就是别让业务代码到处散落 track()。简单点可以用声明式埋点,复杂点可以封装 Hook,再往上就是发布订阅,让业务只抛事件,埋点层统一消费。发布订阅解耦的是业务事件和埋点实现之间的关系。

7 虚拟滚动与大数据列表

简历原文:利用无限加载或虚拟滚动方案,优化大数据量列表加载页面,有效解决复杂业务场景下,图文列表页面的卡顿问题,性能提升达 60%。

问题背景

  • 核心矛盾:列表数据量大(成百上千条图文混排),一次性渲染所有 DOM 导致:
    • DOM 节点过多 → 内存占用高、GC 频繁
    • 首次渲染时间长 → 白屏 / 卡顿
    • 滚动时重排重绘开销大 → 掉帧不流畅

解决方案一:无限加载(Infinite Scroll)

  • 思路:分页 + 滚动触底自动加载下一页,控制单次渲染量
  • 实现:
    • IntersectionObserver 监听底部哨兵元素,进入视口时触发 loadMore()
    • 每次追加一页数据(如 20 条),而非一次加载全部
    • 配合 loading 状态 + 无更多数据提示
  • 局限:页面滚动越久 DOM 越多,本质上只延缓了问题,长列表场景最终还是会卡

解决方案二:虚拟滚动(Virtual Scroll)

  • 核心思路:只渲染可视区域 + 上下缓冲区的 DOM 节点,滚动时动态替换内容
  • 原理:
    • 外层容器设固定高度 + overflow: auto
    • 内层用一个撑开总高度的占位元素(height = itemCount × itemHeight),让滚动条正常
    • 根据 scrollTop 计算当前可视范围的起止索引(startIndex / endIndex
    • 只渲染这个范围内的列表项,用 transform: translateY() 定位到正确位置
    • 滚动时实时更新索引和偏移量 → DOM 数量始终恒定
  • 常用库:
    • react-window — 轻量(~6KB),API 简单,适合大多数场景
    • react-virtualized — 功能全(表格、网格、自动尺寸),体积稍大
    • react-virtuoso — 开箱即用,自动测量高度,不定高最方便
  • 代码示例:
    • 定高:<FixedSizeList height={500} itemCount={10000} itemSize={50}>{renderRow}</FixedSizeList>
    • 不定高:<VariableSizeList itemSize={index => getHeight(index)}>...</VariableSizeList>

不定高度场景处理

  • 难点:图文混排每条高度不同,无法预知 itemSize
  • 方案:
    • 先给预估高度,渲染后用 ResizeObserver 测量真实高度并缓存
    • const ro = new ResizeObserver(entries => { entries.forEach(e => updateHeight(e.contentRect.height)) })
    • 更新高度缓存后通知虚拟列表重新计算偏移(resetAfterIndex()
    • 或直接用 react-virtuoso,它内置了自动测量逻辑

性能提升 60% 怎么衡量

  • 衡量维度:
    • 首屏渲染时间:从全量渲染 1000+ DOM → 只渲染可视区 20-30 个,FCP/LCP 大幅缩短
    • 滚动帧率:Performance 面板看 FPS,优化前掉到 20-30fps,优化后稳定 55-60fps
    • 内存占用:DevTools Memory 快照对比,DOM 节点数从数千降到几十
    • 交互响应:INP 指标改善,点击/滑动不再有明显延迟
可追问点
  • 图片懒加载怎么配合?
    • 虚拟列表项进入可视区时才加载图片(loading="lazy" 或 IntersectionObserver)
    • 配合图片占位(固定宽高比)防止 CLS

8 动画选型

简历原文:通过动画方案调研与选型,解决多种场景下的动画表现力与页面性能的平衡问题,并解决兼容性问题,提升用户访问 PV。

动画压缩与优化 → 详见侧边栏 03 动画压缩

选型原则

  • CSS transition:简单过渡、悬停效果(hover/focus 状态切换)、Vue Router 路由切换动画(<Transition> 组件)
  • CSS animation:循环动画、关键帧序列(loading、呼吸灯等)
  • JS 动画库:多元素联动、交互驱动动画
  • 序列帧动画(精灵图):粒子装饰效果,方案稳定,无 JS 开销
  • 视频(video 标签):静默背景动画,设计直出素材,无需代码实现
方案 适用场景 JS 主线程 GPU 合成 重排重绘 资源体积
CSS3 动画 过渡、悬停、简单循环
项目中广泛采用,性能优秀且无体积消耗
☆☆☆☆☆ ★★★★★ ☆☆☆☆☆ ☆☆☆☆☆
JS 动画库
GSAP / TweenMax
序列联动
项目中用于复杂联动动画
★★★★☆ ★★★☆☆ ★★★☆☆ ★★★☆☆
序列帧动画(精灵图) 帧动画、图标动效
项目中用于粒子装饰效果,方案稳定
☆☆☆☆☆ ★★★★☆ ★☆☆☆☆ ★★★☆☆
视频 已有素材、简单展示
项目中用于 video 标签静默视频
☆☆☆☆☆ ★★☆☆☆ ☆☆☆☆☆ ★★★★★
Lottie
JSON 动画
设计导出的复杂矢量动画
项目未采用,工程取舍
★★★☆☆ ★★☆☆☆ ☆☆☆☆☆ ★★☆☆☆

★ 越多 = 消耗越高(GPU 合成列:★ 越多 = 利用越好)

  • CSS3 动画:不走 JS 主线程,scripting 消耗为零
    GPU 加速方法
    • transform: translateZ(0)translate3d(0,0,0) — 强制创建合成层,触发 GPU 加速
    • will-change: transform — 提前告知浏览器该元素会变化,提前分配合成层
    • opacity 变化天然走合成线程,不触发 layout / paint
    • backface-visibility: hidden — 隐藏背面渲染,减少合成开销
    • 只动 transform / opacity,避免动 width / height / top / left 等触发重排的属性
    • 注意:合成层不是越多越好,过多会增加 GPU 显存消耗(layer explosion)
  • JS 动画库:TweenMax(GreenSock)处理复杂联动效果
    JS 动画性能优化方法
    • requestAnimationFrame 替代 setTimeout/setInterval
      浏览器每秒刷新 60 次(每帧 16.6ms)。setTimeout 的执行时机不固定,可能在帧中间触发导致丢帧或卡顿。rAF 会在浏览器下一次重绘之前执行回调,保证动画更新和屏幕刷新同步,画面更流畅。
    • 动画属性尽量操作 transform / opacity
      浏览器渲染分三步:Layout(计算位置大小)→ Paint(画像素)→ Composite(合成层叠加)。transformopacity 只触发最后的 Composite,跳过前两步,开销最小。
    • 批量读写 DOM,避免强制同步布局(forced reflow)
      如果代码交替"读 DOM 属性 → 写 DOM 样式 → 再读",浏览器每次读之前都要强制重新计算布局。正确做法是先批量读完所有需要的值,再统一写入修改。
    • 复杂计算用 Web Worker 搬离主线程
      JS 是单线程的,如果动画中有大量计算会阻塞主线程导致掉帧。Web Worker 在独立线程中执行计算,算完通过 postMessage 传回主线程。
    • 页面不可见时暂停动画:监听 visibilitychange
      用户切到其他标签页时,动画仍在跑会浪费 CPU/GPU 资源。监听 document.visibilityState === 'hidden' 时暂停动画,'visible' 时恢复。

9 延伸考点

React 性能优化

React.memo / useCallback / useMemo

  • React.memo:对组件做浅比较,props 不变则跳过重渲染
    • 默认情况下父组件重渲染,所有子组件都会跟着重渲染,即使 props 没变
    • React.memo(Component) 会在渲染前浅比较新旧 props,相同就跳过
    • 适合:接收简单 props 的纯展示组件、列表中的每一项
    • 注意:如果传的 props 是对象/函数,每次父组件渲染都会创建新引用,浅比较会认为"变了",memo 就失效了 → 所以要配合 useCallback / useMemo
  • useCallback:缓存函数引用,避免子组件因引用变化而重渲染
    • const handleClick = useCallback(() => { ... }, [deps])
    • deps 不变时,返回同一个函数引用,传给 memo 子组件时不会触发重渲染
    • 典型场景:父组件把事件处理函数传给子组件列表项
    • 不需要到处用:如果子组件没有被 memo 包裹,useCallback 没有意义,反而多了一层缓存开销
  • useMemo:缓存计算结果,避免每次渲染重复计算
    • const result = useMemo(() => heavyCalc(data), [data])
    • data 不变时直接返回上次的结果,跳过计算
    • 适合:大数组过滤/排序、复杂数据转换、传给子组件的派生对象(避免每次创建新引用)
    • 不适合:简单计算(比如字符串拼接),缓存本身有开销,收益不大

状态管理优化 — 减少不必要的重渲染范围

  • 状态下沉:把状态放到真正需要它的组件里,而不是提升到顶层。状态变化只触发局部重渲染
  • 拆分 Context:一个大 Context 里任何值变化,所有消费者都重渲染。拆成多个小 Context(比如 ThemeContext、UserContext 分开),缩小影响范围
  • 选择性订阅:用 zustand / jotai 等状态库的 selector,组件只订阅自己需要的字段,其他字段变化不触发重渲染

列表渲染优化

  • 稳定的 key:用唯一业务 ID 做 key,不要用 index。index 做 key 在列表增删时会导致错误复用和不必要的重渲染
  • 虚拟滚动:大列表用 react-window / react-virtuoso,只渲染可视区域 DOM
  • 分页 / 无限加载:不一次性渲染全部数据,按需加载

避免不必要的渲染触发 — 常见踩坑点

  • 内联对象/数组:style={{ color: 'red' }} 每次渲染都创建新对象 → 提取为常量或用 useMemo
  • 内联函数:onClick={() => handleClick(id)} 每次渲染都是新引用 → 用 useCallback 或把 id 通过 data 属性传递
  • useEffect 依赖:依赖数组里放对象/数组引用会导致每次渲染都执行 effect → 依赖具体的值字段,或用 useRef 存引用
  • setState 传同一个值:React 用 Object.is 比较,传同一个对象引用不会触发更新,但传新对象即使内容相同也会触发

React DevTools Profiler — 定位渲染性能问题

  • 打开 React DevTools → Profiler → 点 Record → 操作页面 → 停止
  • 火焰图显示每个组件的渲染耗时,灰色 = 没渲染,颜色越深 = 渲染越耗时
  • "Why did this render?" 功能直接告诉你组件重渲染的原因(props 变了 / state 变了 / 父组件渲染了)
  • Settings 里勾选 "Highlight updates when components render" 可以实时看到哪些组件在闪烁重渲染
网络优化 — HTTP/1.1 → HTTP/2 → HTTP/3(QUIC) 演进 + CDN
  • HTTP/1.1 的问题:
    • 每个请求独占一个 TCP 连接(或队头阻塞排队),浏览器对同域名并发连接数有限(通常 6 个)
    • 头部冗余:每次请求都带完整 Header(Cookie、User-Agent 等),重复传输
    • 只能客户端主动拉,服务端无法主动推
  • HTTP/2:
    • 多路复用:一个 TCP 连接上并行传输多个请求/响应,不再受"每域名 6 连接"限制
    • 头部压缩(HPACK):客户端和服务端维护一张头部索引表,重复字段只传索引号,大幅减少头部体积
    • 服务器推送:服务端可以在客户端请求 HTML 时,主动推送它将需要的 CSS/JS(实际使用较少,Chrome 已移除支持)
    • 二进制分帧:数据以二进制帧传输,解析更高效
    • 局限:底层仍是 TCP,一旦丢包,整个 TCP 连接上所有流都被阻塞(TCP 层队头阻塞)
  • HTTP/3(QUIC):
    • 核心变化:把传输层从 TCP 换成了 QUIC(基于 UDP),解决 TCP 队头阻塞
    • 独立流:每个流独立,一个流丢包不影响其他流,多路复用更彻底
    • 0-RTT / 1-RTT 握手:QUIC 把 TLS 握手和传输握手合并,首次连接 1-RTT,重连可以 0-RTT,连接建立更快
    • 连接迁移:用 Connection ID 而非 IP+端口标识连接,切换 WiFi/4G 不断连
    • 头部压缩升级:QPACK 替代 HPACK,适配无序到达的特点
    • 现状:主流浏览器已支持,Cloudflare / Akamai / Google 等 CDN 已默认开启
  • CDN 原理:
    • 用户请求 → DNS 解析到离用户最近的边缘节点
    • 边缘节点缓存命中 → 直接返回,延迟极低
    • 缓存未命中 → 回源到原站拉取,缓存后再返回
    • 对前端的意义:静态资源(JS/CSS/图片/字体)放 CDN,用户就近获取,配合强缓存 + hash 文件名效果最好
从输入 URL 到页面显示,发生了什么

1. URL 解析

  • 浏览器判断输入的是 URL 还是搜索词
  • 补全协议(如 https://),解析出协议、域名、端口、路径
  • 检查 HSTS 列表,如果命中则强制 HTTPS

2. DNS 解析

  • 浏览器缓存 → 系统缓存(hosts)→ 路由器缓存 → ISP DNS → 递归查询
  • 递归查询:根域名服务器 → 顶级域(.com)→ 权威 DNS → 拿到 IP
  • 优化点:dns-prefetch 提前解析第三方域名

3. 建立 TCP 连接

  • 三次握手:SYN → SYN+ACK → ACK
  • 如果是 HTTPS,还要进行 TLS 握手(协商加密算法、交换密钥、验证证书)
  • HTTP/2 在一个 TCP 连接上多路复用;HTTP/3 用 QUIC(UDP)可以 0-RTT

4. 发送 HTTP 请求

  • 构造请求行(GET /path HTTP/2)、请求头(Host、Cookie、Accept 等)
  • 浏览器先检查强缓存(Cache-Control / Expires),命中就直接用本地缓存,不发请求
  • 未命中则发请求,可能带 If-None-Match / If-Modified-Since协商缓存,服务端返回 304 则用缓存

5. 服务端处理并返回响应

  • 服务端处理请求(路由、鉴权、查数据库、渲染模板等)
  • 返回状态码 + 响应头 + 响应体(HTML 文档)
  • 如果是 301/302 则浏览器跟随重定向,重新走一遍上面的流程

6. 解析 HTML,构建 DOM 树

  • 浏览器边接收边解析(流式解析),将 HTML 标签转为 DOM 节点
  • 遇到 <link> CSS → 并行下载,不阻塞 DOM 解析,但阻塞渲染
  • 遇到 <script> → 默认阻塞 DOM 解析(要等下载 + 执行完)
  • 优化:defer 并行下载、DOM 解析完再执行;async 并行下载、下载完立即执行
  • 遇到图片/视频等 → 异步下载,不阻塞解析

7. 构建 CSSOM

  • CSS 下载完成后解析为 CSSOM(CSS Object Model)
  • CSSOM 构建是阻塞渲染的:在 CSSOM 构建完之前,浏览器不会渲染任何内容(防止 FOUC 无样式闪烁)

8. 构建 Render Tree(渲染树)

  • DOM + CSSOM 合并生成 Render Tree
  • 只包含可见节点(display:none 不在渲染树中,visibility:hidden 在)
  • 每个节点附带计算后的样式信息

9. Layout(布局 / 重排)

  • 计算每个节点的精确位置和大小(x, y, width, height)
  • 从根节点开始递归,依据盒模型、flex/grid 布局等规则
  • 触发重排的操作:改变宽高、字体大小、增删 DOM、读取 offsetWidth/scrollTop 等

10. Paint(绘制)

  • 将渲染树转为屏幕上的像素:绘制文字、颜色、边框、阴影、图片等
  • 绘制顺序:背景色 → 背景图 → 边框 → 子元素 → outline
  • 触发重绘的操作:改变颜色、背景、visibility 等不影响布局的属性

11. Composite(合成)

  • 浏览器将页面分成多个图层(Layer),独立绘制后由 GPU 合成最终画面
  • transformopacitywill-change 会创建独立合成层,变化时只需重新合成,跳过 Layout 和 Paint → 性能最好
  • 这就是为什么动画优先用 transform / opacity 的原因

12. JS 执行与交互

  • JS 引擎(V8)解析执行脚本,可能修改 DOM/CSSOM → 触发重排重绘
  • 事件循环(Event Loop)处理用户交互、定时器、网络回调等异步任务
  • 页面进入可交互状态(TTI)
一句话版:URL 解析 → DNS 查 IP → TCP + TLS 握手 → 发 HTTP 请求(先查缓存)→ 拿到 HTML → 解析 DOM + 下载 CSS/JS → 构建 CSSOM → DOM + CSSOM 合成渲染树 → Layout 布局 → Paint 绘制 → Composite 合成 → JS 执行、页面可交互。

内存泄漏防治

  • JS 层面:清定时器、移除事件监听、避免全局变量、闭包引用置 null、DOM 引用置 null
  • React 层面:useEffect 返回清理函数、AbortController 取消请求、虚拟滚动避免 DOM 堆积