大型游戏官网与活动页面 · 简历关键词速查
1 首屏加载优化
注水屏 vs 骨架屏
-
骨架屏(Skeleton):页面未加载完时展示占位
UI,减少白屏感知,纯视觉优化
实现方式
- Webpack 插件(如
page-skeleton-webpack-plugin):构建时用 Puppeteer 打开页面,截取 DOM 轮廓自动生成骨架屏 HTML,注入到模板中
- Webpack 插件(如
-
注水屏:页面加载期间展示网易 Logo 的注水填充动画,给用户明确的加载进度感知,比白屏或静态 loading 体验更好
实现方式
- Logo 轮廓用 SVG path 绘制,内部用
clip-path裁剪出形状 - CSS
@keyframes控制一个色块从底部向上填充,配合wave波浪效果模拟注水 - JS 监听资源加载进度(或用定时器模拟),同步驱动填充高度,加载完成后淡出过渡到真实页面
- Logo 轮廓用 SVG path 绘制,内部用
- 区别:骨架屏模拟页面布局结构做占位;注水屏是品牌化的加载动画,两者都是减少白屏感知的视觉过渡方案
其他首屏优化手段
- 代码分割:按路由/组件拆分 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 张大图),不要滥用,否则反而抢占其他资源带宽
-
在 <head> 中加
- 图片优化:懒加载 + 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 实例,配置
rootMargin和threshold -
const observer = new IntersectionObserver(callback, { rootMargin: '200px', threshold: 0 }) -
对目标元素调用
observer.observe(el),进入视口时触发回调加载真实内容 -
加载完成后调用
observer.unobserve(el)停止监听,避免重复触发 - 优势:不靠频繁监听 scroll 事件,由浏览器原生判断,性能开销更低
-
创建 observer 实例,配置
可追问点
不定高度、大数据量场景 → 详见侧边栏 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 放大后,取整误差也被放大,抖动更明显
-
什么是亚像素:屏幕的最小显示单位是 1
个物理像素,但 CSS 计算出的位置可能是小数,比如
-
项目中的方案:用 SVG 包裹序列帧
-
将精灵图作为
<image>放进 SVG 中,通过 SVG 的<animate>或 CSS 控制viewBox/ 裁切区域来切换帧 - SVG 内部坐标系是矢量的、精确的,不存在 CSS background-position 的亚像素取整问题
- 每一帧的裁切位置由 SVG 坐标精确控制,不会因为浏览器像素对齐策略而产生偏移
-
将精灵图作为
-
为什么 SVG 不抖:
-
CSS
background-position在不同 DPR 下会做像素对齐(snap to pixel),导致每帧偏移不一致 - SVG 的坐标系统独立于设备像素,位置计算在矢量空间完成后才光栅化,帧切换位移完全精确
- SVG 天然支持任意缩放不失真,高 DPI 适配也不需要额外处理
-
CSS
-
对比其他方案:
-
CSS
steps(N)— 能解决补间问题,但亚像素偏移在某些设备上仍存在 -
JS 逐帧切换
<img>src — 可行但请求多、内存开销大 - SVG 包裹 — 精度最高、兼容性好、无额外请求,是项目最终采用的方案
-
CSS
-
抖动原因:亚像素渲染
-
图片压缩:可以用 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.com和b.example.com是同站),跨站 = 不同站点之间(比如example.com和other.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 需要带认证信息
-
同源 vs 同站:cookie
的限制是按"站点"(site)而不是"源"(origin)。同站 = 协议
+ eTLD+1 相同(比如
- 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.jsx、utils.js、style.css
各是一个 module
|
| Chunk | 一组 Module 的集合,是 Webpack 内部的分组单位,决定哪些模块打到一起 | 打包阶段 | entry chunk、vendor chunk、动态 import 产生的 async chunk |
| Bundle | Chunk 经过编译压缩后输出的最终文件,是浏览器真正加载的产物 | 输出阶段 |
main.abc123.js、vendor.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(浅灰)— 空闲时间,占比高说明页面有性能余量
-
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)) -
底层是
PerformanceObserverAPI,监听浏览器原生性能事件 -
也可以用
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 虚拟滚动与大数据列表
问题背景
-
核心矛盾:列表数据量大(成百上千条图文混排),一次性渲染所有 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 动画选型
动画压缩与优化 → 详见侧边栏 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(合成层叠加)。transform和opacity只触发最后的 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 合成最终画面
-
transform、opacity、will-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 堆积