前端性能优化 15 条实用技巧

从资源加载到运行时,全方位提升网站性能

前端性能直接影响用户体验和业务指标。研究表明,页面加载时间每增加 1 秒,转化率下降 7%,跳出率增加 30%。性能优化不是玄学,而是有明确方法论的工程实践。本文总结了 15 条最实用的前端性能优化技巧,从资源加载到运行时,覆盖了绝大多数场景。

一、资源加载优化

1. 压缩与合并资源

这是最基础也是性价比最高的优化。HTML、CSS、JavaScript 都应该在构建时进行压缩,移除空格、注释和冗余字符。

// webpack 生产模式自动压缩
// mode: 'production'

// Vite 开箱即用,无需配置
// 构建时自动压缩 JS 和 CSS

// 额外配置:移除 console 和 debugger
// vite.config.js
export default {
  build: {
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  }
}

2. 图片优化

图片通常占页面总大小的 60% 以上,优化图片往往能带来最大的性能收益。

<!-- 响应式图片 + 现代格式 + 懒加载 -->
<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" 
       srcset="image-300.jpg 300w, image-600.jpg 600w, image-1200.jpg 1200w"
       sizes="(max-width: 600px) 100vw, 50vw"
       loading="lazy"
       decoding="async"
       alt="描述"
       width="800"
       height="600">
</picture>

别忘了设置 widthheight 属性,浏览器可以在图片加载前就预留空间,避免布局偏移(CLS)。

3. 代码分割与按需加载

不要把所有代码打包成一个巨大的 JS 文件。利用 ES Module 的动态导入,按路由或功能拆分代码块,用户只加载当前页面需要的代码。

// React 路由级懒加载
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  );
}

// 交互触发时加载
function handleClick() {
  import('./heavy-module').then(mod => {
    mod.doSomething();
  });
}

4. 善用浏览器缓存

合理的缓存策略可以让二次访问几乎秒开。核心原则:静态资源(JS/CSS/图片)设置长期缓存,HTML 不缓存或短缓存。

# Nginx 配置示例
# 静态资源:一年缓存,内容更新通过文件名 hash 实现
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# HTML:不缓存,每次都获取最新版本
location ~* \.html$ {
    add_header Cache-Control "no-cache";
}

现代构建工具(Vite、Webpack)会自动给文件名加 hash,内容变了文件名就变,缓存自然失效。这就是所谓的"指纹缓存策略"。

5. 使用 CDN

CDN(内容分发网络)把资源缓存到全球各地的边缘节点,用户从最近的节点获取资源,延迟可以降低几十到几百毫秒。静态资源、图片、视频都应该走 CDN。常用的 CDN 服务有 Cloudflare、AWS CloudFront、阿里云 CDN 等。

二、渲染性能优化

6. 关键 CSS 内联

CSS 是阻塞渲染的资源——浏览器必须等 CSS 加载解析完才能绘制页面。把首屏需要的关键 CSS 内联在 HTML 中,其余 CSS 异步加载,可以显著提升首屏渲染速度。

<head>
  <style>
    /* 首屏关键样式内联 */
    body { margin: 0; font-family: sans-serif; }
    .header { background: #fff; padding: 16px; }
    .hero { text-align: center; padding: 40px 0; }
  </style>
  <!-- 非关键 CSS 异步加载 -->
  <link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/styles.css"></noscript>
</head>

7. 减少重排重绘

DOM 操作会触发重排(reflow)和重绘(repaint),这是前端性能的常见瓶颈。

// ❌ 不好:每次循环都触发重排
for (let i = 0; i < items.length; i++) {
  element.innerHTML += '<div>' + items[i] + '</div>';
}

// ✅ 好:批量更新,只触发一次重排
let html = '';
for (let i = 0; i < items.length; i++) {
  html += '<div>' + items[i] + '</div>';
}
element.innerHTML = html;

// 或者用 DocumentFragment
const fragment = document.createDocumentFragment();
items.forEach(item => {
  fragment.appendChild(document.createElement('div'));
});
element.appendChild(fragment);

8. 虚拟列表(长列表优化)

当列表有几千甚至几万条数据时,渲染所有 DOM 节点会导致页面卡顿。虚拟列表只渲染可视区域内的元素,大大减少 DOM 节点数量。

// 虚拟列表核心思路
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 offsetY = startIndex * itemHeight;

  return (
    <div style={{ height: containerHeight, overflow: 'auto' }}
         onScroll={e => setScrollTop(e.target.scrollTop)}>
      <div style={{ height: items.length * itemHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map(item => (
            <div style={{ height: itemHeight }}>{item.content}</div>
          ))}
        </div>
      </div>
    </div>
  );
}

实际项目中推荐使用成熟的虚拟列表库:react-virtualized、vue-virtual-scroller 等。

三、JavaScript 运行时优化

9. 防抖和节流

频繁触发的事件(scroll、resize、input)如果每次都执行复杂操作,会导致页面卡顿。防抖(debounce)和节流(throttle)是两大法宝。

// 防抖:事件触发后等待 n 秒再执行,期间再次触发则重新计时
function debounce(fn, delay = 300) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 节流:n 秒内只执行一次
function throttle(fn, interval = 300) {
  let last = 0;
  return function(...args) {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}

// 应用
const handleSearch = debounce(query => fetchData(query), 500);
const handleScroll = throttle(() => updatePosition(), 100);

10. Web Worker:把计算移出主线程

JavaScript 是单线程的,大量计算会阻塞 UI 渲染。Web Worker 可以在后台线程运行脚本,适合处理大数据、复杂计算等任务。

// 主线程
const worker = new Worker('/worker.js');
worker.postMessage(largeData);
worker.onmessage = e => {
  console.log('计算完成:', e.data);
};

// worker.js
self.onmessage = e => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

11. 合理使用 requestAnimationFrame

对于动画和 DOM 更新,使用 requestAnimationFrame 而不是 setTimeout,可以确保动画与浏览器刷新率同步,避免掉帧。

// 平滑滚动到顶部
function smoothScrollToTop() {
  const scrollTop = document.documentElement.scrollTop;
  if (scrollTop > 0) {
    window.requestAnimationFrame(smoothScrollToTop);
    window.scrollTo(0, scrollTop - scrollTop / 8);
  }
}

四、网络与协议优化

12. 使用 HTTP/2 或 HTTP/3

HTTP/2 支持多路复用、头部压缩、服务器推送等特性,相比 HTTP/1.1 有显著的性能提升。HTTP/3 基于 QUIC 协议,性能更好。确保你的服务器启用了 HTTP/2(现在大部分 CDN 和托管平台默认支持)。

13. 预加载关键资源

<!-- 预加载关键字体 -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>

<!-- 预加载首屏图片 -->
<link rel="preload" href="/hero-image.webp" as="image">

<!-- 预连接第三方域名(提前建立 TCP 连接) -->
<link rel="preconnect" href="https://api.example.com">

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com">

14. 字体优化

字体加载会导致文字闪烁(FOIT)或布局偏移(FOUT)。使用 font-display: swap 先用系统字体显示,自定义字体加载完成后再替换。

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;  /* 关键 */
}

15. 性能监控与度量

无法度量就无法优化。关注 Web Vitals 三大核心指标:

// 简单的性能监控
if ('performance' in window) {
  window.addEventListener('load', () => {
    const timing = performance.timing;
    const loadTime = timing.loadEventEnd - timing.navigationStart;
    const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
    console.log(`页面加载: ${loadTime}ms, DOM就绪: ${domReady}ms`);
  });
}

推荐工具:Lighthouse(Chrome DevTools 内置)做全面性能审计;WebPageTest 测试不同地区和网络环境下的表现;DevToolHub 的 图片压缩工具可以快速优化图片大小。

五、构建与工程优化

16. Tree Shaking:移除无用代码

Tree shaking 是 ES Module 带来的福利——打包时自动移除没有被使用的代码,减少包体积。

// ✅ 好:按需导入,tree shaking 会移除没用到的方法
import { debounce, throttle } from 'lodash-es';

// ❌ 不好:全量导入,所有代码都会被打包
import _ from 'lodash';

// 确认配置
// vite.config.js / rollup 默认支持 tree shaking
// webpack 需要设置 mode: 'production' 且使用 ES Module

// package.json 中标记副作用
// {
//   "sideEffects": false  // 没有副作用
//   "sideEffects": ["*.css"]  // 只有 CSS 文件有副作用
// }

注意:只有 ES Module(import/export)才能 tree shaking,CommonJS(require/module.exports)不行。所以尽量使用 ES Module 版本的库,比如 lodash-es 而不是 lodash。

17. 合理选择第三方库

第三方库是包体积膨胀的重灾区。引入库之前先考虑:

使用 bundlephobia.com 可以查询任意 npm 包的体积,决定是否值得引入。BundleAnalyzer 则可以可视化地分析你项目的包组成,找出哪些库占比最大。

18. 预取与预加载策略

<!-- prefetch:预判用户接下来可能需要的资源,闲时加载 -->
<link rel="prefetch" href="/next-page.js" as="script">

<!-- preload:当前页面马上需要的关键资源,高优先级加载 -->
<link rel="preload" href="/critical-font.woff2" as="font" crossorigin>

<!-- prerender:预判用户下一个页面,提前渲染整个页面 -->
<link rel="prerender" href="/next-page.html">

策略原则:当前页面关键资源用 preload,下一个页面可能需要的资源用 prefetch。不要滥用 prefetch,否则会浪费带宽。可以根据用户行为来触发——比如用户鼠标悬停在链接上时,才开始 prefetch 目标页面的资源。

六、感知性能优化

19. 骨架屏与加载状态

感知性能是用户主观感受到的速度。即使真实加载时间一样,有骨架屏的页面看起来加载更快,因为用户知道内容正在加载,而不是卡在那里。

/* 骨架屏闪烁动画 */
.skeleton {
    background: linear-gradient(
        90deg,
        #f0f0f0 25%,
        #e0e0e0 50%,
        #f0f0f0 75%
    );
    background-size: 200% 100%;
    animation: skeleton-loading 1.5s infinite;
}

@keyframes skeleton-loading {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

20. 乐观更新

用户点击按钮后,先在界面上显示成功状态,同时在后台发请求。如果请求失败了再回退。这种"乐观更新"策略可以让用户感觉操作很流畅。

async function handleLike(postId) {
  // 立即更新 UI
  setLiked(true);
  setCount(c => c + 1);

  try {
    await api.likePost(postId);
  } catch (e) {
    // 失败了回退
    setLiked(false);
    setCount(c => c - 1);
    showToast('操作失败,请重试');
  }
}

乐观更新适合那些成功率高、失败代价小的操作(点赞、收藏、关注)。对于删除、支付等不可逆操作,还是应该老实等服务端返回结果。

七、自动化性能监控

性能优化不是一次性的工作,而是持续的过程。建立性能监控机制,才能防止性能回退。

常用工具:Web Vitals(Google 官方)、Sentry Performance、Datadog RUM、New Relic。自建方案也不难——用 performance API + PerformanceObserver 采集数据,上报到自己的服务端即可。

最后记住:性能优化是一场没有终点的马拉松。今天优化到了 2 秒,明年功能越堆越多可能又变成 5 秒。建立性能文化,把性能当做日常开发的考量因素,而不是上线前的临时抱佛脚。