🖥️ 浏览器渲染管线 + Core Web Vitals 指标映射 ① 用户交互 点击 / 输入 / 滚动 Event → TaskQueue ② JS 执行 计算 / 状态变更 EventLoop 调度 ③ Style 计算 CSS 匹配规则 RecalculateStyle ④ Layout 布局 几何信息计算 Reflow / 排版 ⑤ Paint 绘制 像素填充到图层 Rasterization ⑥ Composite 图层合成输出 GPU 显示屏幕 📊 Core Web Vitals — 各指标在管线中的位置 FCP 首次内容绘制 浏览器首次绘制 DOM 内容 目标:<1.8s 🟢 对应阶段:⑤ Paint 首次触发 LCP 最大内容绘制 视口内最大可见元素渲染完 目标:<2.5s 🟢 优化:图片/WebFont预加载 CLS 布局偏移 页面加载过程中布局稳定性 目标:<0.1 🟢 优化:设width/height + aspect-ratio INP 交互延迟 用户交互到下一帧的延迟 目标:<200ms 🟢 优化:减少Long Task + Web Worker TBT 总阻塞时间 TTI之前的阻塞时长 目标:<200ms 🟢 优化:代码分割 + 懒加载
浏览器从用户交互到最终渲染的完整管线,以及 Core Web Vitals 五大指标的对应关系和优化方向
← 返回场景决策

⚡ 前端性能优化场景决策

什么时候用 / 什么时候不用 / 边界在哪里 — 12 大核心场景全解析

懒加载 虚拟滚动 防抖节流 Web Worker 缓存策略 预加载 代码分割 图片优化 SSR vs CSR 骨架屏 请求优化 内存泄漏

1. 懒加载 — 延迟加载决策

懒加载 = 快递分批送 先送急需的,不急的后送,不用一次搬完
不用懒加载 = 一次搬完 50个快递一次搬上6楼,累死也搬不完
什么时候用懒加载
  • 长页面图片:商品列表、图库、新闻信息流 — 屏幕外的图片延迟加载
  • 路由级组件:SPA 中非首屏路由组件 — 用 () => import() 懒加载
  • 重型组件:富文本编辑器、图表库、视频播放器 — 用户点击时才加载
  • 弹窗/抽屉内容:Modal 里的内容在打开时才渲染
  • 第三方SDK:地图、支付、客服 — 按需加载而非全量引入
什么时候不用懒加载
  • 首屏核心内容:Hero 图、导航栏 Logo、首屏文字 — 懒了反而白屏
  • SEO 关键图片:搜索引擎需要的 OG 图片 — 懒加载可能不被爬虫识别
  • 小图标/SVG:几 KB 的资源不值得做懒加载,反而增加请求开销
  • 悬浮触发内容:Tooltip/Popover hover 触发 — 懒加载延迟会让体验卡顿

📊 懒加载决策表

资源类型是否懒加载方案原因
首屏 Banner 图❌ 不懒直接 <img>核心体验,延迟不可接受
商品列表图片✅ 懒IntersectionObserver可能几十张,首屏只需5-6张
富文本编辑器✅ 懒动态 import500KB+ JS,不用时不该加载
路由组件✅ 懒Vue: defineAsyncComponent用户可能永远不点那个页面
小图标 (SVG Sprite)❌ 不懒内联 SVG<5KB,懒加载请求开销比资源本身大
Modal 内容✅ 懒v-if 控制可能永远不打开

💻 图片懒加载 — IntersectionObserver

📀 frontend-app
📁 index.html
lazy-load.html
vite.config.js
// 方案1:原生 loading="lazy"(最简单,浏览器原生支持) <img src="hero.jpg" loading="lazy" alt="商品图"> // 方案2:IntersectionObserver(更精细控制) const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; // data-src → src img.classList.remove('lazy'); observer.unobserve(img); // 加载后停止观察 } }); }, { rootMargin: '200px 0px' // 提前200px开始加载,避免看到空白 }); document.querySelectorAll('img.lazy').forEach(img => { observer.observe(img); });
面试金句 路由懒加载(Lazy Loading)是 SPA 性能优化的第一优先级:只有访问该路由时才加载对应组件(打包成独立 chunk),首屏加载时间可降低 50%+。Vite 原生支持动态 import(),不需额外配置。需注意 Suspense + ErrorBoundary 处理加载状态和失败回退。

💻 路由懒加载 — Vue 3

📀 frontend-app
📁 src/router/
index.js
routes.js
// Vue Router 路由懒加载 const routes = [ { path: '/dashboard', // 动态导入,Webpack/Vite 会自动代码分割 component: () => import('../views/Dashboard.vue') }, { path: '/settings', // 带加载状态和错误处理 component: defineAsyncComponent({ loader: () => import('../views/Settings.vue'), loadingComponent: LoadingSpinner, errorComponent: LoadError, delay: 200, // 200ms后才显示loading timeout: 10000 // 10秒超时 }) } ];
面试金句 图片优化是前端性能的重中之重(通常占页面体积 60%+):格式选择(WebP > JPEG/PNG);响应式图片(srcset + sizes 按屏幕加载合适尺寸);懒加载(loading=lazy);CDN 动态缩放。

💻 组件懒加载 — React

📀 frontend-app
📁 src/components/
React-lazy.jsx
Suspense.jsx
// React.lazy + Suspense const HeavyChart = React.lazy(() => import('./HeavyChart')); function App() { const [showChart, setShowChart] = useState(false); return ( <div> <button onClick={() => setShowChart(true)}>查看图表</button> {showChart && ( <React.Suspense fallback={<div>加载中...</div>}> <HeavyChart /> </React.Suspense> )} </div> ); }
面试金句 路由懒加载(Lazy Loading)是 SPA 性能优化的第一优先级:只有访问该路由时才加载对应组件(打包成独立 chunk),首屏加载时间可降低 50%+。Vite 原生支持动态 import(),不需额外配置。需注意 Suspense + ErrorBoundary 处理加载状态和失败回退。
⚠️ 懒加载注意事项
  • CLS 问题:图片懒加载会导致布局偏移(Cumulative Layout Shift),必须设置 width/height 或 aspect-ratio
  • rootMargin 预加载:不要等图片进入视口才加载,提前 200-500px 开始加载
  • SSR 兼容:服务端渲染时 IntersectionObserver 不存在,需要条件判断

2. 虚拟滚动 — 大列表渲染决策

虚拟滚动 = 电梯只显示当前楼层 100层楼,电梯显示屏只显示你所在的楼层
不用虚拟 = 100层楼全部显示 1-100层同时亮着,电梯面板直接烧了
什么时候用虚拟滚动
  • 大数据列表:1000+ 条记录的表格/列表 — 聊天记录、日志、商品列表
  • 固定行高:每行高度一致或可预估 — 最适合虚拟滚动
  • 社交信息流:微信聊天记录、评论列表 — 动态高度也可用(更复杂)
  • 树形控件:文件目录、组织架构 — 大量节点展开时
什么时候不用虚拟滚动
  • 小数据量:< 100 条 — 直接渲染,虚拟滚动反而增加复杂度
  • 不定高度 + 无法预估:富文本内容每行差异巨大 — 实现极其复杂
  • 需要全文搜索:浏览器 Ctrl+F 无法搜索未渲染的 DOM
  • SEO 页面:搜索引擎爬虫无法索引虚拟滚动的隐藏内容
  • 打印需求:需要打印完整列表 — 虚拟内容未渲染无法打印

📊 虚拟滚动决策表

场景数据量行高方案
后台管理表格100-500固定分页(简单够用)
日志查看器10000+固定✅ 虚拟滚动
聊天记录5000+动态✅ 虚拟滚动(动态高度)
商品瀑布流500+不定分段加载 + 触底加载
下拉选择框1000+固定✅ 虚拟滚动(如 vue-virtual-scroller)

💻 Vue 3 + vue-virtual-scroller

📀 frontend-app
📁 src/components/
VirtualScroll.vue
package.json
// 安装 // npm install vue-virtual-scroller <template> <RecycleScroller class="scroller" :items="items" :item-size="54" key-field="id" v-slot="{ item }" > <div class="item">{{ item.name }}</div> </RecycleScroller> </template> <script setup> import { RecycleScroller } from 'vue-virtual-scroller'; import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'; // 模拟 10000 条数据 const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `用户 ${i + 1}` })); </script> <style> .scroller { height: 600px; } .item { height: 54px; padding: 0 16px; display: flex; align-items: center; } </style>
面试金句 虚拟滚动(Virtual Scrolling)只渲染可视区域的 DOM 元素(通常 10-20 个),无论列表有 10 万条数据, DOM 节点数始终不变。核心原理:transform: translateY 模拟滚动位置 + 动态计算 visible range。推荐库:vue-virtual-scroller / react-window。

💻 React + react-window

📀 frontend-app
📁 src/components/
VirtualList.jsx
DataGrid.jsx
import { FixedSizeList } from 'react-window'; const MyList = ({ items }) => ( <FixedSizeList height={600} width="100%" itemCount={items.length} itemSize={54} > {({ index, style }) => ( <div style={style}> {items[index].name} </div> )} </FixedSizeList> );
面试金句 场景:💻 React + react-window。这是前端性能优化的重要技术手段,理解其原理和适用场景是面试高频考点。实战建议:结合 Lighthouse 和 Chrome DevTools Performance 面板验证优化效果。
⚠️ 虚拟滚动常见坑
  • 滚动条不准确:动态高度时滚动条会跳动,需要预估总高度
  • 动画/过渡失效:虚拟列表中 FLIP 动画无法工作(DOM 被复用)
  • 键盘导航:Tab 键可能跳到未渲染区域,需要特殊处理 focus

3. 防抖 vs 节流 — 高频事件处理决策

防抖 = 电梯等人 有人进来就重新等5秒,直到5秒没人来才关门
节流 = 地铁发车 不管多少人,每5分钟发一趟,到点就走
什么时候用防抖 (Debounce)
  • 搜索框输入:用户打字时等停顿 300ms 才发请求 — 避免每敲一个字发一次
  • 窗口 resize:调整窗口大小,停止后重新计算布局
  • 表单校验:用户停止输入后才校验 — 避免边打字边飘红
  • 自动保存:用户停止编辑 1 秒后保存 — 避免每次按键都存
什么时候用节流 (Throttle)
  • 滚动事件:scroll 监听 — 每 100ms 计算一次位置,而非每像素都算
  • 拖拽移动:mousemove — 每 16ms 更新一次位置(约 60fps)
  • 按钮防连点:提交按钮 — 1 秒内只允许点击 1 次
  • 射击游戏:开枪 — 不管你点得多快,每 200ms 只能开一枪

📊 防抖 vs 节流决策表

维度防抖 (Debounce)节流 (Throttle)
核心逻辑最后一次才执行固定间隔执行
执行次数可能 0 次(一直在动)保证有执行(间隔到了就跑)
适合场景"等用户停了再做""别太频繁,但得持续响应"
搜索框❌(太频繁还是会发请求)
滚动监听❌(停止才响应太迟钝)
按钮防连❌(最后一次才执行=用户以为没反应)
自动保存可用但不够优

💻 防抖实现

📀 frontend-app
📁 src/utils/
debounce.js
throttle.js
function debounce(fn, delay = 300) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; } // Vue 3 使用 <script setup> import { ref } from 'vue'; const searchQuery = ref(''); // 使用 lodash 或自己实现 const handleSearch = debounce(async (query) => { const res = await fetch(`/api/search?q=${query}`); // ... 处理结果 }, 300); // watch 监听输入变化 watch(searchQuery, (val) => handleSearch(val)); </script>
面试金句 防抖(debounce)是等到最后一次操作后再执行,适合搜索框/窗口 resize;节流(throttle)是固定频率执行,适合滑动监听/scroll。Lodash debounce/throttle经过充分测试,不要自己造轮子。

💻 节流实现

📀 frontend-app
📁 src/utils/
throttle.js
debounce.js
function throttle(fn, interval = 100) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= interval) { lastTime = now; fn.apply(this, args); } }; } // 滚动监听使用 window.addEventListener('scroll', throttle(() => { const scrollTop = document.documentElement.scrollTop; // 更新回到顶部按钮的显示状态 showBackToTop.value = scrollTop > 300; }, 100));
面试金句 防抖(debounce)是等到最后一次操作后再执行,适合搜索框/窗口 resize;节流(throttle)是固定频率执行,适合滑动监听/scroll。Lodash debounce/throttle经过充分测试,不要自己造轮子。
💭 类比总结:防抖 = 电梯等人(等最后一个人进来),节流 = 地铁发车(到点就走不管有多少人)

4. Web Worker — 计算密集型任务决策

主线程 = 前台客服 只负责接待和展示,不能被卡住
Worker = 后台仓库 繁重的活交给仓库慢慢干,干完通知前台
什么时候用 Web Worker
  • 大数据处理:10万行 CSV 解析、JSON 深度遍历 — 超过 100ms 的计算
  • 图片处理:Canvas 像素操作、滤镜、裁剪 — 像素级操作很重
  • 加密计算:RSA 加密、哈希计算 — 纯 CPU 运算
  • 复杂排序/搜索:全文搜索索引构建、大数据排序
  • 音视频处理:WebRTC 降噪、音频波形分析
什么时候不用 Web Worker
  • 轻量计算:< 50ms — Worker 通信开销(postMessage 序列化)比计算本身还大
  • DOM 操作:Worker 没有 DOM 访问,只能在主线程操作
  • 简单异步请求:fetch/axios 本身就是异步的,不会阻塞主线程
  • 频繁通信:主线程和 Worker 之间大量小消息 — 序列化/反序列化开销大
  • 共享状态:多个组件需要共享同一份数据 — Worker 数据不好同步

📊 Web Worker 决策表

任务类型耗时用 Worker?替代方案
100条数据过滤<10ms直接在主线程
1000条数据排序~50ms⚠️ 看情况requestIdleCallback
10万行CSV解析200ms+Web Worker
图片滤镜100ms+OffscreenCanvas + Worker
fetch请求异步主线程 async/await

💻 Web Worker 基本用法

📀 frontend-app
📁 src/workers/
heavy-calc.worker.js
image-process.worker.js
// worker.js — Worker 文件 self.onmessage = function(e) { const { data, action } = e.data; if (action === 'parseCSV') { // 耗时操作在 Worker 中执行,不阻塞主线程 const result = data.split('\n').map(row => row.split(',')); self.postMessage({ action: 'parseComplete', result }); } }; // main.js — 主线程 const worker = new Worker('./worker.js'); // 发送数据给 Worker worker.postMessage({ action: 'parseCSV', data: csvString }); // 接收 Worker 处理结果 worker.onmessage = function(e) { if (e.data.action === 'parseComplete') { console.log('解析完成:', e.data.result); // 更新UI } };
面试金句 Web Worker 将 CPU 密集型任务(大数据处理/加密/图片压缩)移至后台线程,不阻塞主线程 UI 响应。Comlink 库封装 postMessage 通信,让 Worker 像异步函数一样调用。

💻 Vite 中使用 Worker

📀 frontend-app
📁 src/components/
WorkerDemo.vue
worker.js
// Vue 3 + Vite 使用 Worker <script setup> import CsvWorker from './csv.worker?worker' const worker = new CsvWorker() worker.postMessage({ action: 'parse', data: largeCsvText }) worker.onmessage = (e) => { parsedData.value = e.data // 更新响应式数据 } // 组件卸载时清理 Worker onUnmounted(() => worker.terminate()) </script>
面试金句 Web Worker 将 CPU 密集型任务(大数据处理/加密/图片压缩)移至后台线程,不阻塞主线程 UI 响应。Comlink 库封装 postMessage 通信,让 Worker 像异步函数一样调用。

5. 前端缓存策略决策

内存缓存 = 口袋里的钱 随取随用,最快,但换了衣服就没了
SessionStorage = 当天的包 关掉tab就清空,只管当天
LocalStorage = 家里的保险箱 长期保存,关浏览器还在,但容量有限
IndexedDB = 银行金库 存大量数据,结构化存储,操作稍慢
什么时候用什么缓存
  • 变量/Map:组件内临时数据、函数计算结果 — 生命周期最短
  • SessionStorage:表单临时数据、当前会话的筛选条件 — 关 tab 就不要了
  • LocalStorage:用户偏好(主题/语言)、Token、功能开关 — 跨会话持久
  • IndexedDB:离线数据、大量结构化数据(邮件/笔记)、文件缓存
  • HTTP 缓存:静态资源(JS/CSS/图片)— 强缓存 + 协商缓存配合
  • Service Worker:PWA 离线缓存、请求拦截 — 离线可用

📊 缓存策略决策表

数据类型大小持久性方案示例
API 响应<5MB会话级内存 Map + TTL商品列表5分钟缓存
用户偏好<1KB永久LocalStorage暗色主题/语言
表单草稿<5MB会话级SessionStorage写了一半的文章
离线数据>50MB永久IndexedDB邮件/笔记离线存储
静态资源不限永久HTTP 强缓存JS/CSS 带 hash
敏感信息--❌ 不缓存密码/银行卡号

💻 内存缓存 + TTL 工具函数

📀 frontend-app
📁 src/utils/
MemoryCache.js
fetch-cache.js
// 简单的内存缓存,带过期时间 class MemoryCache { constructor() { this.cache = new Map(); } set(key, value, ttlMs = 5 * 60 * 1000) { // 默认5分钟 this.cache.set(key, { value, expiry: Date.now() + ttlMs }); } get(key) { const item = this.cache.get(key); if (!item) return null; if (Date.now() > item.expiry) { this.cache.delete(key); // 过期自动清理 return null; } return item.value; } clear() { this.cache.clear(); } } // Vue 3 组合式使用 const apiCache = new MemoryCache(); async function fetchWithCache(url, ttlMs) { const cached = apiCache.get(url); if (cached) return cached; const res = await fetch(url); const data = await res.json(); apiCache.set(url, data, ttlMs); return data; }
面试金句 计算属性缓存(Vue computed / React useMemo)避免不必要的重复计算。关键原则:只在计算成本远大于缓存成本时才使用。避免在 computed 中做副作用(如发起请求),computed 应该是纯函数。

6. 预加载 vs 预获取 — 提前加载决策

preload = 出门前把伞放包里 确定马上要用,提前装好
prefetch = 出门前查了天气预报 可能要用,空闲时先准备
preconnect = 先拨号不通就挂 先建立连接(DNS+TCP+TLS),不发请求
什么时候用 preload
  • 关键字体:自定义字体必须 preload — 否则 FOIT(文字闪烁)
  • 首屏关键 CSS:内联关键CSS,preload 其余样式
  • 首屏大图/视频:Hero 图片 — preload 优先级最高
  • 关键 JS 模块:首屏交互需要的 JS — preload 让它尽早下载
什么时候用 prefetch
  • 下一页路由:用户可能点击的下一个页面 — 空闲时预加载
  • 弹窗内容:大概率会打开的帮助文档
  • 分页数据:第2页数据 — 用户很可能翻页
什么时候不用
  • prefetch 所有路由:浪费带宽 — 只 prefetch 最可能的下一个页面
  • preload 第三方分析SDK:非关键资源 — 抢占首屏资源带宽
  • 移动端大量 prefetch:用户流量有限 — 慎重使用

📊 预加载策略决策表

策略优先级触发时机典型场景
preconnect最高页面加载时第三方域名(CDN、API)
preload当前页面需要关键字体、Hero图、CSS
prefetch低(空闲时)下个页面可能需要路由组件、下页数据
dns-prefetch最低未来可能访问第三方域名DNS预解析

💻 HTML 预加载标签

📀 frontend-app
📁 public/
index.html
robots.txt
<!-- preconnect: 提前建连,省掉 DNS + TCP + TLS 时间 --> <link rel="preconnect" href="https://cdn.example.com"> <link rel="dns-prefetch" href="https://analytics.example.com"> <!-- preload: 当前页面必须用的资源 --> <link rel="preload" href="/fonts/custom-font.woff2" as="font" crossorigin> <link rel="preload" href="/hero-image.webp" as="image"> <link rel="preload" href="/critical.css" as="style"> <!-- prefetch: 下一个页面可能需要的资源 --> <link rel="prefetch" href="/js/dashboard-chunk.js" as="script">
面试金句 资源预加载策略:preload 告诉浏览器这个资源当前页面立即需要(高优先级下载);prefetch 告诉空闲时预加载下个路由的资源(低优先级)。Vite 自动生成 prefetch 提示用于路由懒加载 chunk。

💻 Vue Router 预取下一个路由

📀 frontend-app
📁 src/router/
hover-prefetch.js
index.js
// 悬停时预加载路由组件 const router = createRouter({ routes: [ { path: '/dashboard', component: () => import('./Dashboard.vue') // Vite 自动 prefetch } ] }); // 手动在鼠标悬停时预加载 function prefetchRoute(routeName) { const route = router.resolve({ name: routeName }); const link = document.createElement('link'); link.rel = 'prefetch'; link.href = route.href; document.head.appendChild(link); } // <router-link @mouseover="prefetchRoute('dashboard')">
面试金句 资源预加载策略:preload 告诉浏览器这个资源当前页面立即需要(高优先级下载);prefetch 告诉空闲时预加载下个路由的资源(低优先级)。Vite 自动生成 prefetch 提示用于路由懒加载 chunk。

7. 代码分割 — 打包优化决策

代码分割 = 快递分箱 急的先寄,不急的后寄,不用一箱装完
不分割 = 一箱全塞 一个2MB的JS,首屏只需要200KB也得全下
什么时候做代码分割
  • 路由级分割:每个页面独立 chunk — 最基本、必须做
  • 第三方库拆分:vue/react/echarts 单独打包 — 利用浏览器缓存
  • 按功能拆分:编辑器模块、图表模块、地图模块 — 按需加载
  • 条件代码:A/B测试代码、管理后台 — 非全量用户需要
什么时候不用/过度分割
  • 碎文件:每个小组件一个 chunk — HTTP/2 虽然多路复用,但太多小文件仍浪费
  • 首屏必须的代码:不要异步加载首屏核心逻辑
  • 高频切换页面:用户频繁在 A/B 页面切换 — 合并更好

💻 Vite 代码分割配置

📀 frontend-app
📁
vite.config.js
package.json
// vite.config.js import { defineConfig } from 'vite' export default defineConfig({ build: { rollupOptions: { output: { // 手动分割第三方库 manualChunks: { 'vue-vendor': ['vue', 'vue-router', 'pinia'], 'ui-vendor': ['element-plus'], 'chart-vendor': ['echarts'], 'utils': ['lodash-es', 'dayjs'], } } }, // chunk 大小警告阈值 chunkSizeWarningLimit: 500 } })
面试金句 代码拆分(Code Splitting)是减小首屏 JS 体积的核心手段:路由级拆分(每个路由独立 chunk)+ 组件级拆分(大组件独立加载)+ 第三方库拆分(vendor chunk 独立缓存)。webpack-bundle-analyzer / rollup-plugin-visualizer 可视化分析打包体积。
⚠️ 代码分割最佳实践
  • vendor chunk 不变:第三方库单独打包,业务代码更新时不影响 vendor 缓存
  • chunk 大小:单个 chunk 建议不超过 200KB(gzip 前),超过考虑再拆
  • 初始加载:首屏 JS 总量 < 200KB(gzip) 为佳

8. 图片优化 — 格式与加载决策

图片优化 = 快递包装 能用纸箱别用木箱,能塞小别寄大
什么时候用什么格式
  • WebP:照片、商品图、背景图 — 比 JPEG/PNG 小 30-50%,兼容性好
  • AVIF:极致压缩场景 — 比 WebP 再小 20%,但编码慢、兼容性差
  • SVG:图标、Logo、插画 — 矢量无损,代码可编辑,无限缩放
  • PNG:需要透明通道的小图 — 图标 Sprite、简单 UI 元素
  • JPEG:照片 — 不需要透明度的大图(WebP 不支持时回退)
  • GIF → MP4:动画 — GIF 体积巨大,转 MP4 可以小 90%

📊 图片格式决策表

格式体积对比透明动画兼容性适用场景
WebP基准97%首选通用格式
AVIF小20%92%极致压缩
JPEG大30-50%100%照片回退
PNG大50-200%100%小图标/透明
SVG极小100%图标/Logo

💻 picture 标签 — 格式降级

📀 frontend-app
📁 public/
responsive-images.html
index.html
<!-- 浏览器从上到下选第一个支持的 --> <picture> <source srcset="hero.avif" type="image/avif"> <source srcset="hero.webp" type="image/webp"> <img src="hero.jpg" alt="Hero" width="800" height="450"> </picture> <!-- 响应式图片 --> <img srcset="small.webp 400w, medium.webp 800w, large.webp 1200w" sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px" src="medium.webp" alt="响应式图片" >
面试金句 图片优化是前端性能的重中之重(通常占页面体积 60%+):格式选择(WebP > JPEG/PNG);响应式图片(srcset + sizes 按屏幕加载合适尺寸);懒加载(loading=lazy);CDN 动态缩放。

💻 图片优化 Checklist

📀 frontend-app
📁
vite.config.js
package.json
// 1. 压缩 — Vite 插件 // npm install vite-plugin-imagemin --save-dev // 2. 尺寸 — CSS 设置最大宽 // img { max-width: 100%; height: auto; } // 3. 避免用 img 缩放 — 不要传 2000px 图给 CSS 显示 200px // <img src="2000px.jpg" style="width:200px"> ❌ 浪费! // <img src="200px.jpg"> ✅ 用实际需要的尺寸 // 4. CSS 背景图 — 小图标用 base64 内联 .icon { background-image: url("data:image/svg+xml;base64,..."); }
面试金句 图片优化是前端性能的重中之重(通常占页面体积 60%+):格式选择(WebP > JPEG/PNG);响应式图片(srcset + sizes 按屏幕加载合适尺寸);懒加载(loading=lazy);CDN 动态缩放。

9. SSR vs CSR — 渲染模式决策

SSR = 服务端生成完整HTML 服务器返回可直接渲染的HTML,首屏快但每次需请求
CSR = 自助餐食材自做 给你食材(JS),你自己做(浏览器渲染)
SSG = 便当预制 提前做好的便当,拿起来就吃
什么时候用 SSR
  • SEO 要求高:电商商品页、博客文章、营销页 — 搜索引擎需要完整 HTML
  • 首屏速度关键:内容型网站 — 用户期望立即看到内容
  • 社交分享:需要 OG 标签预览 — SSR 才能输出 meta 标签
  • 低性能设备:用户设备差 — 减少客户端渲染压力
什么时候用 CSR
  • 后台管理系统:不需要 SEO — CSR 更简单
  • 交互密集型:在线编辑器、实时协作 — CSR 延迟更低
  • 动态内容为主:个性化仪表盘 — SSR 缓存命中率低
  • 开发速度优先:快速 MVP — CSR 开发成本低

📊 SSR vs CSR vs SSG 决策表

维度SSRCSRSSG
首屏速度最快
SEO
服务器压力最小
交互响应中等中等
动态内容
开发复杂度
典型场景电商/博客后台/工具文档/博客
框架Nuxt/NextVite SPANuxt/Next SSG

💻 Vue 3 SSR 基本示例 (Nuxt 3)

📀 frontend-app
📁
nuxt.config.ts
package.json
// nuxt.config.ts export default defineNuxtConfig({ // 默认就是 SSR 模式 ssr: true, // 某些页面用 CSR routeRules: { '/admin/**': { ssr: false }, // 管理后台不需要SSR '/blog/**': { isr: 3600 }, // 博客1小时ISR缓存 } }) // pages/blog/[id].vue <script setup> // useAsyncData 只在服务端执行一次 const { data: article } = await useAsyncData( `article-${route.params.id}`, () => $fetch(`/api/articles/${route.params.id}`) ) // useSeoMeta 设置 SEO 标签 useSeoMeta({ title: article.value.title, ogTitle: article.value.title, description: article.value.summary, }) </script>
面试金句 前端缓存多层架构:HTTP 缓存(Cache-Control/ETag)→ Service Worker 缓存(离线可用)→ CDN 缓存(边缘节点)→ 浏览器内存缓存。缓存策略的核心是平衡新鲜度和性能:静态资源用强缓存 + hash 文件名,动态数据用协商缓存。

10. 骨架屏 — 加载体验决策

骨架屏 = 先展示页面占位结构 降低用户感知等待,内容到达后填充
转圈Loading = 让你干等 啥也不告诉你,等着吧
什么时候用骨架屏
  • 内容区域加载:卡片列表、文章内容、用户信息 — 有固定布局
  • 首屏加载:SPA 进入时的首屏内容 — 减少白屏时间感
  • 数据驱动区域:需要等 API 返回的区域 — 骨架屏给用户预期
什么时候不用骨架屏
  • 快速加载:< 200ms 就能渲染 — 骨架屏闪一下反而更差
  • 不确定布局:内容形状变化大 — 骨架屏和实际内容差异大不如不做
  • 全屏Loading:初始化/登录 — 不如用品牌动画
  • 操作反馈:按钮提交后 — 用按钮 loading 状态,不是骨架屏

💻 CSS 骨架屏动画

📀 frontend-app
📁 src/styles/
skeleton.css
loading.css
/* 骨架屏基础样式 */ .skeleton { background: linear-gradient( 90deg, #2a2a3a 25%, #3a3a4a 50%, #2a2a3a 75% ); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 4px; } @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } /* Vue 3 组件 */ <template> <div v-if="loading" class="skeleton-card"> <div class="skeleton" style="width:60px;height:60px;border-radius:50%"></div> <div style="flex:1"> <div class="skeleton" style="width:40%;height:16px;margin-bottom:8px"></div> <div class="skeleton" style="width:80%;height:12px;margin-bottom:4px"></div> <div class="skeleton" style="width:60%;height:12px"></div> </div> </div> <slot v-else /> </template>
面试金句 场景:💻 CSS 骨架屏动画。这是前端性能优化的重要技术手段,理解其原理和适用场景是面试高频考点。实战建议:结合 Lighthouse 和 Chrome DevTools Performance 面板验证优化效果。

🖥️ Chrome DevTools Performance 面板解读

⚡ Performance — my-app.com (优化前)
📊 Performance Recording... ⚙️ Settings ● 4.2s Total
0ms500ms1.0s1.5s2.0s2.5s3.0s3.5s4.0s
Main
Raster
Network
Parse HTML Script Layout/Paint Long Task ⚠️ GPU Raster
FCP
3.1s
LCP
5.2s
CLS
0.15
TBT
780ms
Long Tasks
12个 ⚠️

问题诊断:Main 线程在 77%~95% 区间有 Long Task(红色),对应 JS 阻塞渲染 → 导致 FCP/LCP 偏高

11. 请求优化 — 网络请求场景决策

请求合并 = 拼车 3个人去同一个方向,拼一辆车
请求取消 = 不上车了 搜索框还没打完,上一个请求取消掉
什么时候做请求优化
  • 竞态请求:搜索框连续输入 — 取消前一个请求(AbortController)
  • 批量请求:页面初始化多个独立接口 — Promise.all 并发
  • 重复请求:多个组件请求同一接口 — 合并为一次(请求去重)
  • 分页数据:后台管理列表 — 只请求当前页,不一次全拉
  • 大文件上传:>5MB 文件 — 分片上传 + 断点续传

💻 AbortController — 取消竞态请求

📀 frontend-app
📁 src/hooks/
useSearch.js
useDebounce.js
// 搜索时取消上一次请求 let abortController = null; async function searchProducts(query) { // 取消上一次还没完成的请求 if (abortController) { abortController.abort(); } abortController = new AbortController(); try { const res = await fetch(`/api/search?q=${query}`, { signal: abortController.signal }); return await res.json(); } catch (err) { if (err.name === 'AbortError') { console.log('请求被取消'); // 正常取消,不报错 } else { throw err; // 真正的错误要抛出 } } }
面试金句 场景:💻 AbortController — 取消竞态请求。这是前端性能优化的重要技术手段,理解其原理和适用场景是面试高频考点。实战建议:结合 Lighthouse 和 Chrome DevTools Performance 面板验证优化效果。

💻 请求去重 — 避免重复请求

📀 frontend-app
📁 src/utils/
request-dedup.js
fetch-cache.js
// 多个组件同时请求同一接口,只发一次 const pendingRequests = new Map(); async function fetchOnce(url, options = {}) { const key = url + JSON.stringify(options); if (pendingRequests.has(key)) { return pendingRequests.get(key); // 复用正在进行的请求 } const promise = fetch(url, options).then(res => { pendingRequests.delete(key); // 完成后移除 return res.json(); }).catch(err => { pendingRequests.delete(key); throw err; }); pendingRequests.set(key, promise); return promise; } // 3个组件同时调用,只发1次请求 const data1 = await fetchOnce('/api/user/info'); const data2 = await fetchOnce('/api/user/info'); // 复用上面的请求
面试金句 场景:💻 请求去重 — 避免重复请求。这是前端性能优化的重要技术手段,理解其原理和适用场景是面试高频考点。实战建议:结合 Lighthouse 和 Chrome DevTools Performance 面板验证优化效果。

💻 分片上传

📀 frontend-app
📁 src/utils/
upload-chunks.js
file-utils.js
async function uploadLargeFile(file) { const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB 一片 const totalChunks = Math.ceil(file.size / CHUNK_SIZE); for (let i = 0; i < totalChunks; i++) { const start = i * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end); const formData = new FormData(); formData.append('file', chunk); formData.append('chunkIndex', i); formData.append('totalChunks', totalChunks); formData.append('fileId', file.name + '-' + file.size); await fetch('/api/upload/chunk', { method: 'POST', body: formData }); // 更新上传进度 uploadProgress.value = Math.round(((i + 1) / totalChunks) * 100); } // 所有分片上传完,通知后端合并 await fetch('/api/upload/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileId: file.name + '-' + file.size, totalChunks }) }); }
面试金句 代码拆分(Code Splitting)是减小首屏 JS 体积的核心手段:路由级拆分(每个路由独立 chunk)+ 组件级拆分(大组件独立加载)+ 第三方库拆分(vendor chunk 独立缓存)。webpack-bundle-analyzer / rollup-plugin-visualizer 可视化分析打包体积。

12. 内存泄漏排查 — 生命周期管理决策

内存泄漏 = 房间只扔垃圾不倒 每次进房间扔一点,从不清,最后满到住不了
正确清理 = 定期打扫 离开房间时带走垃圾,保持空间
常见内存泄漏场景
  • 未取消的定时器:setInterval/setTimeout 组件卸载后还在跑
  • 未移除的事件监听:addEventListener 没有对应的 removeEventListener
  • 闭包引用:闭包里引用了大对象,对象永远无法 GC
  • DOM 引用:JS 变量持有已移除 DOM 节点的引用
  • 全局变量:往 window 上挂数据,永远不会被回收
  • 未断开的 WebSocket:组件卸载后 WebSocket 还在接收数据

📊 内存泄漏排查决策表

症状可能原因排查工具修复方案
页面越来越卡定时器/监听器未清理DevTools Memory → Heap SnapshotonUnmounted 中清理
切换路由后内存不降组件引用未释放Performance → Memory 实时曲线检查全局 Map/Set 引用
长时间操作后崩溃数组无限增长Heap Snapshot 对比限制数组长度/分页
频繁GC卡顿大量临时对象Performance Monitor → GC频率对象池/减少创建

🖥️ Chrome DevTools Memory 堆快照对比

🧠 Memory — Heap Snapshot 对比
📸 快照 #1:页面刚加载
构造函数数量占用
(string)1,2402.1 MB
Array860.4 MB
Object1420.3 MB
(closure)240.1 MB
总计~1,4922.9 MB ✅
操作 10 次
切换路由
📸 快照 #2:操作 10 次后
构造函数数量占用
(string)3,6808.4 MB ⚠️
Array2862.1 MB ⚠️
TimerHandler ⚠️101.8 MB 💀
EventListener ⚠️100.9 MB 💀
总计~3,98613.2 MB 🔴
🔍 泄漏分析:
TimerHandler 从 0 → 10 个:每次进入页面创建 setInterval 但组件卸载时没 clearInterval
EventListener 从 0 → 10 个:addEventListener 没配对 removeEventListener
(string) 从 2.1MB → 8.4MB:闭包里引用的大字符串无法被 GC 回收
内存增长 4.5 倍,操作越多越卡,最终浏览器标签页崩溃

💻 Vue 3 常见泄漏修复

📀 frontend-app
📁 src/components/
TimerLeak.vue
Dashboard.vue
// ❌ 泄漏:定时器没清理 onMounted(() => { setInterval(() => { fetchLatestData(); }, 5000); }); // ✅ 修复:组件卸载时清理 onMounted(() => { const timer = setInterval(() => { fetchLatestData(); }, 5000); onUnmounted(() => clearInterval(timer)); }); // ❌ 泄漏:事件监听没移除 window.addEventListener('resize', handleResize); // ✅ 修复:卸载时移除 onMounted(() => { window.addEventListener('resize', handleResize); }); onUnmounted(() => { window.removeEventListener('resize', handleResize); }); // ✅ 更优雅:使用 VueUse // npm install @vueuse/core import { useEventListener, useIntervalFn } from '@vueuse/core' // 自动在卸载时清理 useEventListener(window, 'resize', handleResize); useIntervalFn(fetchLatestData, 5000); // 自动清理
面试金句 场景:💻 Vue 3 常见泄漏修复。这是前端性能优化的重要技术手段,理解其原理和适用场景是面试高频考点。实战建议:结合 Lighthouse 和 Chrome DevTools Performance 面板验证优化效果。

💻 React 泄漏修复

📀 frontend-app
📁 src/components/
TimerCleanup.jsx
Dashboard.jsx
// ✅ useEffect 清理函数 useEffect(() => { const timer = setInterval(() => { fetchLatestData(); }, 5000); const controller = new AbortController(); // 清理函数:组件卸载时执行 return () => { clearInterval(timer); controller.abort(); }; }, []); // 空依赖 = 只在挂载/卸载时执行
面试金句 场景:💻 React 泄漏修复。这是前端性能优化的重要技术手段,理解其原理和适用场景是面试高频考点。实战建议:结合 Lighthouse 和 Chrome DevTools Performance 面板验证优化效果。
⚠️ 内存泄漏排查工具
  • Chrome DevTools → Memory:拍 Heap Snapshot,对比两次快照找到增长的对象
  • Performance Monitor:实时看 JS Heap Size 曲线,持续增长 = 泄漏
  • Allocation Timeline:记录内存分配时间线,定位分配热点
  • 思路:操作页面 → 拍快照1 → 重复操作 → 拍快照2 → 对比找增长

⚡ 前端性能优化场景决策 · 全栈资料库

← 返回场景决策