多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE

技术文章

聚焦一线编程技术原理与实战踩坑,沉淀可复用的工程经验。

前端性能优化:从首屏加载到运行时

前端性能优化:从首屏加载到运行时

前端性能优化分两条线:让首屏更快加载(加载性能),让页面运行更流畅(运行时性能)。前者解决"白屏等待",后者解决"操作卡顿"。本文从资源压缩、懒加载到虚拟列表、防抖节流,配合 Web Vitals 指标,给你一套可落地的优化手册。

一、性能优化的衡量标准:Web Vitals

优化前先会测量。Google 提出的 Core Web Vitals 是当前主流指标:

  • LCP(Largest Contentful Paint):最大内容渲染时间,<2.5s 为佳,衡量首屏加载体验。
  • FID/INP(Interaction to Next Paint):交互响应延迟,<200ms 为佳,衡量交互流畅度。
  • CLS(Cumulative Layout Shift):累计布局偏移,<0.1 为佳,衡量视觉稳定性。
// 用 web-vitals 库采集真实用户指标
import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(console.log);
onINP(console.log);
onCLS(console.log);

// 或用 PerformanceObserver 监听 LCP
new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const last = entries[entries.length - 1];
  console.log('LCP:', last.startTime, last.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });

二、资源压缩:减小体积是第一步

传输体积直接决定加载时间。压缩策略贯穿构建全流程:

// vite.config.js / webpack.config.js 关键项
export default {
  build: {
    minify: 'terser',       // JS 压缩
    cssCodeSplit: true,     // CSS 按需拆分
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router'],  // 第三方拆包
        }
      }
    }
  }
}
  • JS/CSS 压缩:terser、cssnano 去空格、删注释、缩短变量。
  • 图片压缩:WebP/AVIF 比 JPEG 小 30%-50%,体积显著下降。
  • Gzip/Brotli:服务器开启压缩,文本资源再减 70%。
  • Tree Shaking:ES Module 静态分析,剔除未用代码。

三、懒加载:按需加载,延后开销

首屏只加载首屏需要的资源,其余延后。路由懒加载与图片懒加载是两大杀器。

// 路由懒加载:按需拆分 chunk
const routes = [
  { path: '/', component: () => import('./views/Home.vue') },
  { path: '/about', component: () => import('./views/About.vue') },
];

// 图片懒加载:原生 loading 属性
<img src="hero.jpg" loading="lazy" alt="首屏图">          <!-- 首屏用 eager -->
<img src="news-1.jpg" loading="lazy" alt="资讯图">      <!-- 视口外延迟加载 -->

// IntersectionObserver 自定义懒加载
const io = new IntersectionObserver((entries) => {
  entries.forEach(e => {
    if (e.isIntersecting) {
      e.target.src = e.target.dataset.src;
      io.unobserve(e.target);
    }
  });
});
document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));

四、虚拟列表:长列表渲染神器

渲染上万条数据时,DOM 节点过多是卡顿元凶。虚拟列表只渲染可视区域的几十条,滚动时动态替换,DOM 数量恒定。

// 虚拟列表核心思路
function VirtualList({ items, itemHeight, viewportH }) {
  const [scrollTop, setScrollTop] = useState(0);

  const startIdx = Math.floor(scrollTop / itemHeight);
  const visibleCount = Math.ceil(viewportH / itemHeight) + 2; // 缓冲
  const endIdx = startIdx + visibleCount;
  const visible = items.slice(startIdx, endIdx);

  return (
    <div style={{ height: viewportH, overflow: 'auto' }}
         onScroll={e => setScrollTop(e.target.scrollTop)}>
      <div style={{ height: items.length * itemHeight, position: 'relative' }}>
        {visible.map((it, i) => (
          <div key={startIdx + i}
               style={{ position: 'absolute', top: (startIdx + i) * itemHeight, height: itemHeight }}>
            {it.text}
          </div>
        ))}
      </div>
    </div>
  );
}

五、防抖与节流:控制高频触发

搜索框输入、窗口 resize、滚动事件会高频触发回调,造成性能浪费。防抖节流是经典解法。

// 防抖:连续触发只执行最后一次(适合搜索联想)
function debounce(fn, delay = 300) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
input.addEventListener('input', debounce(fetchSuggest, 300));

// 节流:固定间隔最多执行一次(适合滚动、resize)
function throttle(fn, interval = 200) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}
window.addEventListener('scroll', throttle(onScroll, 200));

区别:防抖"等停下来再执行",节流"按固定频率执行"。搜索用防抖,滚动用节流。

六、运行时优化:减少重排重绘

  • 批量 DOM 操作:用 DocumentFragment 或一次性 className 切换,避免多次触发布局。
  • 避免强制同步布局:不要在修改样式后立即读取 offsetTop/clientWidth,会触发强制重排。
  • requestAnimationFrame:动画用它调度,对齐刷新率,避免丢帧。
  • transform/opacity 做动画:它们走合成层,不触发重排。
// 差:逐个添加节点,触发多次重排
data.forEach(item => list.appendChild(createLi(item)));

// 好:DocumentFragment 一次性插入
const frag = document.createDocumentFragment();
data.forEach(item => frag.appendChild(createLi(item)));
list.appendChild(frag);

// 动画用 transform,硬件加速
.box { transition: transform .3s; }
.box.move { transform: translateX(100px); }  /* 而非 left: 100px */

七、缓存策略:让重复访问秒开

合理的缓存能让二次访问近乎瞬开。配合强缓存与协商缓存分层处理。

# 静态资源:带 hash 文件名,强缓存一年
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType application/javascript "access plus 1 year"
  ExpiresByType text/css "access plus 1 year"
  ExpiresByType image/webp "access plus 1 year"
</IfModule>

// Service Worker 离线缓存(PWA)
const CACHE = 'mzph-v1';
self.addEventListener('install', (e) => {
  e.waitUntil(caches.open(CACHE).then(c => c.addAll(['/','/app.js'])));
});
self.addEventListener('fetch', (e) => {
  e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));
});

性能优化没有银弹,核心是"测量—定位—优化—验证"的闭环。先用 Lighthouse 与 Web Vitals 找到瓶颈,再对症下药:首屏慢从资源体积与加载策略入手,交互卡从渲染与事件频率入手。把这些手段沉淀成团队的性能检查清单,性能才会持续可控。

返回列表