前端性能优化面试题
前端性能优化是面试中的高频考点,以下涵盖加载优化、渲染优化、网络优化、指标度量等全方位性能优化知识。
1. 前端性能指标有哪些?如何度量?
markdown
核心 Web 指标(Core Web Vitals):
1. LCP(Largest Contentful Paint)最大内容绘制
- 衡量加载性能:页面最大元素渲染完成的时间
- 良好:≤ 2.5s | 需改进:≤ 4.0s | 差:> 4.0s
2. INP(Interaction to Next Paint)交互到下一帧绘制
- 衡量交互响应性:用户交互到页面响应的时间
- 良好:≤ 200ms | 需改进:≤ 500ms | 差:> 500ms
(替代了原来的 FID)
3. CLS(Cumulative Layout Shift)累计布局偏移
- 衡量视觉稳定性:页面元素意外移动的程度
- 良好:≤ 0.1 | 需改进:≤ 0.25 | 差:> 0.25
javascript
// ========== 使用 Performance API 度量 ==========
// 1. Navigation Timing(页面加载各阶段耗时)
const timing = performance.getEntriesByType('navigation')[0]
console.log({
DNS查询: timing.domainLookupEnd - timing.domainLookupStart,
TCP连接: timing.connectEnd - timing.connectStart,
首字节时间TTFB: timing.responseStart - timing.requestStart,
DOM解析: timing.domInteractive - timing.responseEnd,
DOM完成: timing.domComplete - timing.domInteractive,
页面完全加载: timing.loadEventEnd - timing.fetchStart
})
// 2. 使用 PerformanceObserver 监听性能指标
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
console.log('LCP:', lastEntry.startTime, 'ms')
}).observe({ type: 'largest-contentful-paint', buffered: true })
// CLS
let clsScore = 0
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value
}
}
console.log('CLS:', clsScore)
}).observe({ type: 'layout-shift', buffered: true })
// FP / FCP
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.startTime}ms`)
}
}).observe({ type: 'paint', buffered: true })
// 3. 使用 web-vitals 库(Google 官方)
import { onLCP, onINP, onCLS } from 'web-vitals'
onLCP((metric) => {
console.log('LCP:', metric.value)
// 上报到监控平台
sendToAnalytics({ name: 'LCP', value: metric.value })
})
onINP((metric) => console.log('INP:', metric.value))
onCLS((metric) => console.log('CLS:', metric.value))
常用性能指标一览:
| 指标 | 全称 | 含义 | 良好值 |
|---|---|---|---|
| FP | First Paint | 首次绘制 | - |
| FCP | First Contentful Paint | 首次有内容绘制 | ≤ 1.8s |
| LCP | Largest Contentful Paint | 最大内容绘制 | ≤ 2.5s |
| INP | Interaction to Next Paint | 交互响应 | ≤ 200ms |
| CLS | Cumulative Layout Shift | 布局偏移 | ≤ 0.1 |
| TTFB | Time to First Byte | 首字节时间 | ≤ 800ms |
| TTI | Time to Interactive | 可交互时间 | ≤ 3.8s |
💡 面试加分点: Google 在 2024 年用 INP 替代了 FID 作为核心 Web 指标。FID 只测量首次交互延迟,而 INP 测量整个页面生命周期内所有交互的响应延迟。
2. 首屏加载优化有哪些方案?
javascript
// ========== 1. 代码分割 + 路由懒加载 ==========
// React
const Home = React.lazy(() => import('./pages/Home'))
const About = React.lazy(() => import('./pages/About'))
// Vue
const routes = [
{ path: '/', component: () => import('./views/Home.vue') },
{ path: '/about', component: () => import('./views/About.vue') }
]
// ========== 2. 预加载关键资源 ==========
// <link rel="preload">:高优先级加载当前页面需要的资源
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/api/initial-data" as="fetch" crossorigin>
// <link rel="prefetch">:低优先级加载未来可能需要的资源
<link rel="prefetch" href="/js/about-page.js">
// <link rel="preconnect">:提前建立连接
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
// ========== 3. 关键 CSS 内联 ==========
// 将首屏需要的 CSS 内联到 HTML 中,其余 CSS 异步加载
<style>
/* 首屏关键 CSS 直接内联 */
body { margin: 0; font-family: sans-serif; }
.hero { height: 100vh; display: flex; align-items: center; }
</style>
<link rel="stylesheet" href="/styles/full.css" media="print" onload="this.media='all'">
// ========== 4. 骨架屏(Skeleton Screen)==========
// 在数据加载前显示页面结构轮廓,改善感知速度
function SkeletonCard() {
return (
<div className="skeleton-card">
<div className="skeleton-avatar animate-pulse" />
<div className="skeleton-title animate-pulse" />
<div className="skeleton-text animate-pulse" />
</div>
)
}
// ========== 5. SSR / SSG ==========
// SSR:服务端渲染,首屏直接返回完整 HTML
// SSG:静态站点生成,构建时生成 HTML
// Next.js SSG 示例
export async function getStaticProps() {
const data = await fetchData()
return { props: { data }, revalidate: 60 } // ISR:60秒重新生成
}
// ========== 6. 服务端推送(HTTP/2 Server Push)==========
// 服务器在响应 HTML 时主动推送关键资源
// 已逐渐被 103 Early Hints 替代
3. 资源加载优化策略?
javascript
// ========== 1. 图片优化 ==========
// 使用现代图片格式
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="fallback" loading="lazy">
</picture>
// 响应式图片
<img
srcset="image-320w.jpg 320w,
image-640w.jpg 640w,
image-1280w.jpg 1280w"
sizes="(max-width: 600px) 320px,
(max-width: 1200px) 640px,
1280px"
src="image-640w.jpg"
alt="responsive image"
>
// 图片懒加载
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" alt="">
// 使用 IntersectionObserver 实现懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
observer.unobserve(img)
}
})
}, { rootMargin: '200px' }) // 提前 200px 开始加载
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img))
// ========== 2. JavaScript 加载优化 ==========
// defer:异步加载,DOM 解析完成后按序执行(推荐)
<script defer src="main.js"></script>
// async:异步加载,下载完立即执行(适合独立脚本如统计代码)
<script async src="analytics.js"></script>
// 动态导入 + 预加载
// 用户鼠标移入导航时预加载
navLink.addEventListener('mouseenter', () => {
import(/* webpackPrefetch: true */ './pages/About')
})
// ========== 3. 字体优化 ==========
// 预加载关键字体
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
// 使用 font-display 避免 FOIT(不可见文本闪烁)
@font-face {
font-family: 'MyFont';
src: url('/fonts/main.woff2') format('woff2');
font-display: swap; /* 先用系统字体,字体加载后替换 */
}
// 字体子集化:只包含需要的字符
// 工具:fonttools/pyftsubset
// 中文字体从 10MB+ 减少到几百 KB
// ========== 4. 第三方脚本优化 ==========
// 异步加载第三方脚本
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.async = true
script.onload = resolve
script.onerror = reject
document.body.appendChild(script)
})
}
// 在浏览器空闲时加载非关键第三方脚本
if ('requestIdleCallback' in window) {
requestIdleCallback(() => loadScript('https://analytics.example.com/sdk.js'))
} else {
setTimeout(() => loadScript('https://analytics.example.com/sdk.js'), 3000)
}
4. HTTP 缓存策略如何设计?
markdown
缓存优先级:
Service Worker Cache → Memory Cache → Disk Cache → HTTP 请求
HTTP 缓存分为两类:
1. 强缓存:不发请求,直接使用缓存
2. 协商缓存:发请求验证缓存是否有效
nginx
# Nginx 缓存配置示例
# ===== HTML 文件:不缓存(或短期缓存 + 协商缓存)=====
location / {
add_header Cache-Control "no-cache"; # 每次都验证
# 或 add_header Cache-Control "max-age=0, must-revalidate";
}
# ===== 带 hash 的静态资源:长期强缓存 =====
location /assets/ {
# 文件名包含内容哈希(如 main.a1b2c3.js),内容不变哈希不变
add_header Cache-Control "public, max-age=31536000, immutable";
# immutable:告诉浏览器这个资源永远不会变
}
# ===== API 响应:根据场景选择 =====
location /api/ {
# 不缓存
add_header Cache-Control "no-store";
# 或短期缓存
# add_header Cache-Control "private, max-age=60";
}
javascript
// ========== 缓存策略最佳实践 ==========
// 1. HTML 文件 → no-cache(协商缓存)
// 每次都验证是否更新,保证用户能获取最新版本
// 2. JS/CSS/图片(带 contenthash)→ 强缓存 1 年
// Cache-Control: max-age=31536000, immutable
// 内容变化 → hash 变化 → 新的 URL → 自动更新
// 3. API 数据 → 根据场景
// 实时数据:no-store(不缓存)
// 不常变化的数据:max-age=60 + stale-while-revalidate=300
// ========== 强缓存 vs 协商缓存 ==========
// 强缓存(不发请求)
// Cache-Control: max-age=31536000
// → 200 (from disk cache) / (from memory cache)
// 协商缓存(发请求验证)
// 请求头: If-None-Match: "abc123" / If-Modified-Since: Wed, 21 Oct 2024
// 响应头: ETag: "abc123" / Last-Modified: Wed, 21 Oct 2024
// → 304 Not Modified(缓存有效)或 200(返回新内容)
// ========== Service Worker 缓存(最高优先级)==========
// Cache First 策略
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request).then((response) => {
const clone = response.clone()
caches.open('v1').then((cache) => cache.put(event.request, clone))
return response
})
})
)
})
缓存策略决策树:
| 资源类型 | 缓存策略 | Cache-Control |
|---|---|---|
| HTML | 协商缓存 | no-cache |
| JS/CSS(有 hash) | 长期强缓存 | max-age=31536000, immutable |
| 图片/字体(有 hash) | 长期强缓存 | max-age=31536000 |
| API(实时数据) | 不缓存 | no-store |
| API(变化不频繁) | 短期缓存 | max-age=60, stale-while-revalidate=300 |
5. 渲染优化有哪些手段?
javascript
// ========== 1. 减少重排(Reflow)和重绘(Repaint)==========
// ❌ 频繁操作 DOM 触发多次重排
for (let i = 0; i < 1000; i++) {
document.body.appendChild(document.createElement('div'))
}
// ✅ 使用 DocumentFragment 批量操作
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
fragment.appendChild(document.createElement('div'))
}
document.body.appendChild(fragment) // 只触发一次重排
// ✅ 使用 CSS class 替代多次 style 修改
// ❌ 触发 3 次重排
element.style.width = '100px'
element.style.height = '200px'
element.style.margin = '10px'
// ✅ 只触发 1 次重排
element.className = 'new-style'
// ========== 2. 使用 CSS 合成属性(Composite)==========
// 只触发合成,不触发重排重绘(GPU 加速)
.animated-element {
/* ✅ transform 和 opacity 只触发合成层 */
transform: translateX(100px);
opacity: 0.5;
will-change: transform; /* 提前告知浏览器 */
/* ❌ 以下属性会触发重排 */
/* left: 100px; top: 50px; width: 200px; */
}
// ========== 3. 虚拟列表(Virtual List)==========
// 只渲染可视区域内的元素,解决长列表性能问题
function VirtualList({ items, itemHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0)
const startIndex = Math.floor(scrollTop / itemHeight)
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
items.length
)
const visibleItems = items.slice(startIndex, endIndex)
const totalHeight = items.length * itemHeight
const offsetY = startIndex * itemHeight
return (
<div
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, i) => (
<div key={startIndex + i} style={{ height: itemHeight }}>
{item.name}
</div>
))}
</div>
</div>
</div>
)
}
// 推荐使用成熟的库:react-virtuoso、@tanstack/react-virtual、vue-virtual-scroller
// ========== 4. requestAnimationFrame 动画 ==========
// ✅ 与浏览器刷新率同步,不掉帧
function animate() {
element.style.transform = `translateX(${x}px)`
x += speed
if (x < target) {
requestAnimationFrame(animate)
}
}
requestAnimationFrame(animate)
// ========== 5. requestIdleCallback 空闲时执行任务 ==========
// 在浏览器空闲时执行低优先级任务
function processLargeData(data) {
let index = 0
function processChunk(deadline) {
while (index < data.length && deadline.timeRemaining() > 0) {
processItem(data[index])
index++
}
if (index < data.length) {
requestIdleCallback(processChunk)
}
}
requestIdleCallback(processChunk)
}
// ========== 6. Web Worker 处理 CPU 密集型任务 ==========
// 主线程
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })
worker.postMessage({ type: 'sort', data: largeArray })
worker.onmessage = (e) => {
console.log('排序完成:', e.data)
}
// worker.ts
self.onmessage = (e) => {
if (e.data.type === 'sort') {
const sorted = e.data.data.sort((a, b) => a - b)
self.postMessage(sorted)
}
}
6. React 性能优化有哪些手段?
jsx
// ========== 1. React.memo 避免不必要的重渲染 ==========
const ExpensiveComponent = React.memo(({ data, onItemClick }) => {
console.log('渲染了 ExpensiveComponent')
return (
<ul>
{data.map(item => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
)
})
// ========== 2. useMemo 缓存计算结果 ==========
function ProductList({ products, filterText }) {
// 只有 products 或 filterText 变化时才重新计算
const filteredProducts = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(filterText.toLowerCase())
)
}, [products, filterText])
return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
// ========== 3. useCallback 缓存函数引用 ==========
function Parent() {
const [count, setCount] = useState(0)
// 如果不用 useCallback,每次渲染都会创建新函数
// 导致 React.memo 包裹的子组件也重新渲染
const handleClick = useCallback((id) => {
console.log('clicked:', id)
}, []) // 空依赖,函数引用永不变
return (
<>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<ExpensiveComponent data={items} onItemClick={handleClick} />
</>
)
}
// ========== 4. React.lazy + Suspense 代码分割 ==========
const HeavyChart = React.lazy(() => import('./HeavyChart'))
function Dashboard() {
return (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
)
}
// ========== 5. useTransition / useDeferredValue(React 18+)==========
function SearchPage() {
const [query, setQuery] = useState('')
const [isPending, startTransition] = useTransition()
const handleChange = (e) => {
// 输入框立即更新
setQuery(e.target.value)
// 搜索结果延迟更新(低优先级)
startTransition(() => {
setSearchResults(filterData(e.target.value))
})
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : <SearchResults />}
</>
)
}
// useDeferredValue:延迟更新值
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query)
// deferredQuery 会在浏览器空闲时更新,不阻塞输入
const results = useMemo(() => filterData(deferredQuery), [deferredQuery])
return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
}
// ========== 6. 状态下沉 / 状态提升 ==========
// ❌ 状态放在顶层,整棵树都重渲染
function App() {
const [inputValue, setInputValue] = useState('')
return (
<div>
<input value={inputValue} onChange={e => setInputValue(e.target.value)} />
<ExpensiveTree /> {/* 每次输入都重渲染 */}
</div>
)
}
// ✅ 将频繁变化的状态下沉到小组件中
function SearchInput() {
const [inputValue, setInputValue] = useState('')
return <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
}
function App() {
return (
<div>
<SearchInput /> {/* 只有这个小组件重渲染 */}
<ExpensiveTree /> {/* 不受影响 */}
</div>
)
}
💡 面试加分点: React 19 引入了 React Compiler,会自动为组件添加 memoization,未来可能不需要手动写
useMemo/useCallback。但在此之前,对于大型列表渲染和复杂计算,手动优化仍然很重要。
7. Vue 性能优化有哪些手段?
vue
<!-- ========== 1. v-once:只渲染一次 ========== -->
<template>
<div v-once>
<!-- 这个块只渲染一次,后续更新会跳过 -->
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
</template>
<!-- ========== 2. v-memo:有条件地缓存(Vue 3.2+)========== -->
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
<!-- 只有 item.selected 变化时才重新渲染这个节点 -->
<p>{{ item.name }}</p>
<p>{{ item.selected ? '✅' : '❌' }}</p>
</div>
</template>
<!-- ========== 3. 计算属性缓存 ========== -->
<script setup>
import { computed, shallowRef, shallowReactive } from 'vue'
// computed 有缓存,只有依赖变化才重新计算
const sortedList = computed(() => {
return [...list.value].sort((a, b) => a.score - b.score)
})
// ========== 4. shallowRef / shallowReactive ==========
// 大型对象只需要响应第一层
const hugeData = shallowRef({ list: [], meta: {} })
// 更新时需要替换整个引用
hugeData.value = { ...hugeData.value, list: newList }
// ========== 5. 虚拟列表 ==========
// 使用 vue-virtual-scroller
import { RecycleScroller } from 'vue-virtual-scroller'
</script>
<template>
<RecycleScroller
:items="items"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="item">{{ item.name }}</div>
</RecycleScroller>
</template>
<!-- ========== 6. 组件异步加载 ========== -->
<script setup>
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingSpinner,
delay: 200, // 延迟 200ms 再显示 loading
timeout: 10000, // 超时时间
errorComponent: ErrorDisplay
})
// ========== 7. KeepAlive 缓存组件 ==========
</script>
<template>
<KeepAlive :max="10" :include="['Home', 'Dashboard']">
<router-view />
</KeepAlive>
</template>
<!-- ========== 8. v-show vs v-if ========== -->
<!-- v-show:频繁切换用(只是 display:none,DOM 一直存在) -->
<div v-show="isVisible">频繁切换的内容</div>
<!-- v-if:不频繁切换用(条件为 false 时不渲染 DOM) -->
<div v-if="isLoaded">初始化后不再切换的内容</div>
8. 打包体积优化策略?
javascript
// ========== 1. Tree Shaking ==========
// 确保使用 ES Module 导入
import { debounce } from 'lodash-es' // ✅ 可 Tree Shake
// import _ from 'lodash' // ❌ 整包引入
// ========== 2. 按需引入 UI 组件库 ==========
// 使用 unplugin-vue-components / babel-plugin-import
// ❌ import ElementPlus from 'element-plus' // 全量引入 ~800KB
// ✅ import { ElButton, ElInput } from 'element-plus' // 按需引入
// ========== 3. 代码分割 ==========
// Vite
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-lib': ['element-plus'],
'chart': ['echarts']
}
}
}
}
})
// ========== 4. 压缩 ==========
// JS 压缩:esbuild(Vite 默认)/ terser
// CSS 压缩:cssnano / lightningcss
// HTML 压缩:html-minifier
// 图片压缩:squoosh / sharp / imagemin
// 文本压缩:Gzip / Brotli
// ========== 5. CDN 外置大型依赖 ==========
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}
}
}
})
// index.html 从 CDN 加载
// <script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
// ========== 6. 分析包体积 ==========
// Vite
import { visualizer } from 'rollup-plugin-visualizer'
// Webpack
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
// ========== 7. 动态 polyfill ==========
// 根据浏览器按需加载 polyfill,而非全量引入
<script src="https://polyfill.io/v3/polyfill.min.js?features=es2020"></script>
// ========== 8. 替换体积更小的库 ==========
// moment.js (~300KB) → dayjs (~2KB)
// lodash (~70KB) → lodash-es (Tree Shakeable)
// axios (~13KB) → ky (~3KB) 或原生 fetch
9. 网络优化策略?
javascript
// ========== 1. HTTP/2 多路复用 ==========
// HTTP/1.1:同一域名最多 6 个并发连接
// HTTP/2:单连接多路复用,无并发限制
// 建议:使用 HTTP/2 + 合理的代码分割(不需要合并文件)
// ========== 2. 资源压缩 ==========
// Brotli 压缩比 Gzip 更好(通常小 15-25%)
// Nginx 配置
// gzip on;
// gzip_types text/css application/javascript application/json;
// brotli on;
// brotli_types text/css application/javascript application/json;
// ========== 3. CDN 加速 ==========
// 将静态资源部署到 CDN,利用边缘节点就近访问
// Vite 配置
export default defineConfig({
base: 'https://cdn.example.com/assets/'
})
// ========== 4. 请求优化 ==========
// 接口合并:BFF 层聚合多个接口
// 请求去重:相同请求在 pending 期间复用
class RequestDedup {
pendingMap = new Map()
async request(key, fetcher) {
if (this.pendingMap.has(key)) {
return this.pendingMap.get(key) // 复用 pending 的请求
}
const promise = fetcher().finally(() => this.pendingMap.delete(key))
this.pendingMap.set(key, promise)
return promise
}
}
// ========== 5. 数据预取 ==========
// 在用户可能访问之前预取数据
// React Query
const { data } = useQuery(['user', userId], fetchUser)
// 预取下一页
queryClient.prefetchQuery(['users', page + 1], () => fetchUsers(page + 1))
// ========== 6. WebSocket / SSE 替代轮询 ==========
// ❌ 轮询:每隔 3 秒发请求
setInterval(() => fetch('/api/notifications'), 3000)
// ✅ SSE:服务器主动推送
const eventSource = new EventSource('/api/notifications/stream')
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
updateNotifications(data)
}
// ========== 7. 使用 103 Early Hints ==========
// 服务器在处理完 HTML 之前,先发送 103 提示浏览器预加载资源
// 103 Early Hints
// Link: </style.css>; rel=preload; as=style
// Link: </main.js>; rel=preload; as=script
10. 内存泄漏如何排查和避免?
javascript
// ========== 常见内存泄漏场景 ==========
// 1. 事件监听器未移除
// ❌
function setup() {
window.addEventListener('resize', handleResize)
}
// ✅
function setup() {
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize) // 清理
}
// React 中
useEffect(() => {
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
// Vue 中
onMounted(() => window.addEventListener('resize', handleResize))
onUnmounted(() => window.removeEventListener('resize', handleResize))
// 2. 定时器未清除
// ❌
setInterval(() => fetchData(), 5000)
// ✅
const timer = setInterval(() => fetchData(), 5000)
onUnmounted(() => clearInterval(timer))
// 3. 闭包持有大对象引用
// ❌
function createClosure() {
const hugeArray = new Array(1000000).fill('data')
return function() {
console.log(hugeArray.length) // 闭包持有 hugeArray,无法被 GC
}
}
// ✅ 用完就释放
function createClosure() {
let hugeArray = new Array(1000000).fill('data')
const length = hugeArray.length
hugeArray = null // 释放引用
return function() {
console.log(length)
}
}
// 4. DOM 引用残留
// ❌
const elements = []
function addElement() {
const el = document.createElement('div')
document.body.appendChild(el)
elements.push(el) // JS 中保留了 DOM 引用
}
function removeElement() {
const el = elements.pop()
el.remove() // DOM 移除了,但 elements 数组中的引用还在
}
// 5. Map/Set 未清理
// ❌
const cache = new Map()
function cacheData(key, data) {
cache.set(key, data) // 缓存无限增长
}
// ✅ 使用 WeakMap 或设置上限
const cache = new WeakMap() // key 对象被 GC 时自动清理
// 或 LRU 缓存
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize
this.cache = new Map()
}
get(key) {
if (!this.cache.has(key)) return undefined
const value = this.cache.get(key)
this.cache.delete(key)
this.cache.set(key, value) // 移到最新
return value
}
set(key, value) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey) // 删除最旧的
}
this.cache.set(key, value)
}
}
// ========== 排查工具 ==========
// 1. Chrome DevTools → Memory 面板
// - Heap Snapshot:查看内存快照,比较两个快照找到泄漏
// - Allocation Timeline:记录内存分配时间线
// - Allocation Sampling:采样分析
// 2. Performance Monitor
// Chrome → 更多工具 → 性能监视器
// 观察 JS Heap Size 是否持续增长
11. 白屏时间如何优化?
javascript
// 白屏时间 = 从请求页面到首次渲染内容的时间
// 核心策略:减少关键渲染路径(Critical Rendering Path)
// ========== 1. 减小 HTML 体积 ==========
// 压缩 HTML,移除注释和空白
// ========== 2. 内联关键 CSS ==========
// 将首屏 CSS 内联到 <head> 中,避免 CSS 阻塞渲染
<head>
<style>
/* 首屏关键 CSS */
.header { ... }
.hero { ... }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="/styles/full.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
</head>
// ========== 3. JS 异步加载 ==========
// defer 不阻塞 HTML 解析
<script defer src="/main.js"></script>
// ========== 4. 服务端渲染(SSR)==========
// 直接返回完整 HTML,无需等待 JS 下载执行
// Next.js / Nuxt.js / Remix
// ========== 5. 预渲染(Prerendering)==========
// 构建时生成静态 HTML,适合内容不常变化的页面
// vite-plugin-ssr / @prerenderer/webpack-plugin
// ========== 6. Loading 动画 ==========
// 在 JS 加载前显示 loading,改善用户感知
<div id="app">
<!-- 这个 loading 在 JS 加载前就会显示 -->
<div id="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
</div>
// ========== 7. 使用 CDN + HTTP/2 ==========
// 减少网络延迟
// ========== 8. 减少重定向 ==========
// 每次重定向都会增加一次 RTT
// http → https → www → 实际页面 (三次重定向!)
// 应该直接:https://www.example.com
12. 图片优化最佳实践?
html
<!-- ========== 1. 选择合适的图片格式 ========== -->
<!--
AVIF:最优压缩率,兼容性较新
WebP:优秀压缩率,广泛支持
JPEG:照片类图片的通用选择
PNG:需要透明度的图片
SVG:图标、Logo、简单图形
JPEG XL:未来趋势(浏览器支持有限)
-->
<!-- ========== 2. 响应式图片 ========== -->
<picture>
<!-- 现代格式优先 -->
<source srcset="photo.avif" type="image/avif">
<source srcset="photo.webp" type="image/webp">
<!-- 不同屏幕尺寸 -->
<source srcset="photo-mobile.jpg" media="(max-width: 768px)">
<source srcset="photo-tablet.jpg" media="(max-width: 1200px)">
<img src="photo.jpg" alt="产品图" loading="lazy" decoding="async">
</picture>
<!-- srcset + sizes 实现像素密度适配 -->
<img
srcset="image-1x.jpg 1x, image-2x.jpg 2x, image-3x.jpg 3x"
src="image-2x.jpg"
alt="retina 适配"
>
<!-- ========== 3. 懒加载 ========== -->
<!-- 原生懒加载 -->
<img src="photo.jpg" loading="lazy" alt="">
<!-- 首屏图片使用 eager 加载 + fetchpriority -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="">
<!-- ========== 4. 图片尺寸声明(避免 CLS)========== -->
<!-- 始终声明 width 和 height -->
<img src="photo.jpg" width="800" height="600" alt="">
<!-- 或用 CSS aspect-ratio -->
<style>
.img-container {
aspect-ratio: 16 / 9;
width: 100%;
}
</style>
javascript
// ========== 5. 构建时图片压缩 ==========
// Vite 插件
import viteImagemin from 'vite-plugin-imagemin'
export default defineConfig({
plugins: [
viteImagemin({
gifsicle: { optimizationLevel: 7 },
mozjpeg: { quality: 75 },
pngquant: { quality: [0.65, 0.9] },
svgo: { plugins: [{ removeViewBox: false }] },
webp: { quality: 75 }
})
]
})
// ========== 6. 图片 CDN + 实时裁剪 ==========
// 使用图片 CDN 服务(如 Cloudinary、阿里云 OSS、腾讯云 COS)
// 根据设备自动裁剪和压缩
const imageUrl = `https://cdn.example.com/image.jpg?w=400&h=300&q=80&format=webp`
13. 前端监控体系怎么搭建?
javascript
// ========== 1. 性能监控 ==========
// 使用 web-vitals 采集核心指标
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals'
function reportMetric(metric) {
// 上报到监控平台
navigator.sendBeacon('/api/metrics', JSON.stringify({
name: metric.name,
value: metric.value,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
url: location.href,
timestamp: Date.now()
}))
}
onLCP(reportMetric)
onINP(reportMetric)
onCLS(reportMetric)
onFCP(reportMetric)
onTTFB(reportMetric)
// ========== 2. 错误监控 ==========
// JS 运行时错误
window.addEventListener('error', (event) => {
reportError({
type: 'runtime',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack
})
})
// Promise 未捕获错误
window.addEventListener('unhandledrejection', (event) => {
reportError({
type: 'unhandledrejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack
})
})
// 资源加载错误
window.addEventListener('error', (event) => {
if (event.target && (event.target.src || event.target.href)) {
reportError({
type: 'resource',
url: event.target.src || event.target.href,
tagName: event.target.tagName
})
}
}, true) // 捕获阶段
// ========== 3. 用户行为监控 ==========
// 点击、路由变化、停留时长等
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-track]')
if (target) {
reportEvent({
type: 'click',
element: target.dataset.track,
text: target.textContent?.slice(0, 50)
})
}
})
// ========== 4. 接口监控 ==========
// 重写 fetch / XMLHttpRequest 监控接口性能
const originalFetch = window.fetch
window.fetch = async function(...args) {
const start = performance.now()
try {
const response = await originalFetch.apply(this, args)
const duration = performance.now() - start
reportApi({
url: args[0],
status: response.status,
duration,
success: response.ok
})
return response
} catch (error) {
reportApi({ url: args[0], status: 0, error: error.message })
throw error
}
}
💡 面试加分点: 生产环境推荐使用成熟的监控平台(Sentry、腾讯前端监控 TAM、阿里 ARMS),它们提供了 Source Map 反解析、错误聚合、告警等完整能力。
14. Service Worker 和 PWA 缓存策略?
javascript
// Service Worker 可以拦截网络请求,实现离线缓存
// ========== 注册 Service Worker ==========
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW 注册成功:', reg.scope))
.catch(err => console.error('SW 注册失败:', err))
})
}
// sw.js
const CACHE_NAME = 'app-v1'
const STATIC_ASSETS = [
'/',
'/index.html',
'/css/main.css',
'/js/main.js'
]
// 安装:预缓存核心资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
)
self.skipWaiting() // 立即激活
})
// 激活:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
)
)
})
// ========== 缓存策略 ==========
// 1. Cache First(缓存优先):适合静态资源
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => cached || fetch(event.request))
)
})
// 2. Network First(网络优先):适合 API 数据
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then(response => {
const clone = response.clone()
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone))
return response
})
.catch(() => caches.match(event.request))
)
}
})
// 3. Stale While Revalidate(缓存+后台更新):适合不常变化的资源
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then(cache =>
cache.match(event.request).then(cached => {
const fetched = fetch(event.request).then(response => {
cache.put(event.request, response.clone())
return response
})
return cached || fetched
})
)
)
})
// 推荐使用 Workbox 库简化 SW 开发
// import { precacheAndRoute, registerRoute } from 'workbox-precaching'
// import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
15. 性能优化的整体方法论?
bash
前端性能优化全景图:
┌─────────────────────────────────────┐
│ 网络层 │
│ ├── CDN 加速 │
│ ├── HTTP/2 / HTTP/3 │
│ ├── Gzip / Brotli 压缩 │
│ ├── DNS 预解析 / 预连接 │
│ ├── 合理的缓存策略 │
│ └── 减少请求数和请求体积 │
├─────────────────────────────────────┤
│ 资源层 │
│ ├── 代码分割 + 按需加载 │
│ ├── Tree Shaking │
│ ├── 图片优化(格式/压缩/懒加载) │
│ ├── 字体优化(子集/preload/swap) │
│ ├── 第三方库精简 │
│ └── 预加载关键资源 │
├─────────────────────────────────────┤
│ 渲染层 │
│ ├── 关键 CSS 内联 │
│ ├── JS 异步加载(defer/async) │
│ ├── 减少 DOM 操作 │
│ ├── 避免强制同步布局 │
│ ├── CSS 动画用 transform/opacity │
│ ├── 虚拟列表 │
│ └── SSR / SSG │
├─────────────────────────────────────┤
│ 运行时 │
│ ├── 防抖/节流 │
│ ├── Web Worker │
│ ├── requestIdleCallback │
│ ├── 内存泄漏防治 │
│ └── 框架层面优化(memo/computed等) │
├─────────────────────────────────────┤
│ 体验层 │
│ ├── 骨架屏 │
│ ├── 渐进式加载 │
│ ├── 乐观更新 │
│ ├── 加载状态提示 │
│ └── 离线支持(PWA) │
├─────────────────────────────────────┤
│ 度量层 │
│ ├── Core Web Vitals │
│ ├── Lighthouse │
│ ├── Performance API │
│ ├── Chrome DevTools │
│ └── 前端监控平台 │
└─────────────────────────────────────┘
💡 面试加分点: 性能优化不是一次性的工作,而是一个持续的过程。正确的做法:1)先度量(Lighthouse、Web Vitals)→ 2)分析瓶颈(Performance 面板、BundleAnalyzer)→ 3)针对性优化 → 4)验证效果 → 5)建立监控防止回退。