前端性能优化面试题

前端性能优化面试题

前端性能优化是面试中的高频考点,以下涵盖加载优化、渲染优化、网络优化、指标度量等全方位性能优化知识。


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)建立监控防止回退。

相关推荐
ClouGence1 小时前
文件上传也能录制回放了:CueCast UI 自动化测试更新
前端·测试
沙盘客1 小时前
AFSIM 15篇 调试、性能优化与工程化最佳实践
经验分享·性能优化
禁止摆烂_才浅1 小时前
Vue3 高频面试题
前端·vue.js·面试
天才熊猫君1 小时前
从“改自己”到“改布局”——拖拽调整的通用设计思路
前端·javascript
何时梦醒1 小时前
CSS 三栏布局与 BFC 深度解析:Float、Flex、Grid 三种方案一网打尽
前端·css·面试
橘子星1 小时前
别只会用 Pick/Omit/Partial!从拼多多一道面试题搞懂 TS 全部内置工具类型
前端
嘟嘟07171 小时前
TypeScript 工具类型通关:Pick、Omit、Partial、Record 区别与 Omit 等价实现一次讲清
设计模式·面试·typescript
PC2005_cloud1 小时前
DSH 沙箱机制拆解:受限令牌为何会掐断 HTTPS 请求
前端·程序员
众人皆醒我独醉1 小时前
MLOps 流水线:从训练到推理的 CI/CD
面试·llm·gpu