返回笔记首页

打包体积与白屏时间优化 - 面试完整指南

主题配置

简历描述模板(STAR法则)

模板一:打包体积优化(5MB → 300KB)

主导前端资源体积优化,通过代码分割、依赖替换、Tree Shaking 等手段,将生产环境包体积从 5MB 压缩至 300KB(减少 94%),首屏资源从 5MB 降至 150KB,首屏加载时间从 4.5s 降至 1.2s。

  • 使用 webpack-bundle-analyzer 深度分析,识别 moment.js、lodash、echarts 等大型依赖占用 3.8MB
  • 替换大型库:moment → dayjs(500KB → 7KB),lodash → lodash-es + tree-shaking(300KB → 50KB)
  • 实施路由级代码分割,首屏代码从 5MB 降至 150KB,懒加载覆盖率 100%
  • 配置 Gzip + CDN + 缓存策略,实际传输体积降至 80KB,缓存命中率 85%

模板二:白屏时间优化(3s → <1s)

系统性优化前端加载性能,从资源、渲染、缓存三个维度突破白屏瓶颈。将 FCP 从 2.5s 降至 0.6s,LCP 从 3.0s 降至 0.9s,用户体验评分从 C 级提升至 A+ 级。

  • 实施 SSR 服务端渲染 + Critical CSS 内联,FCP 提升 76%(2.5s → 0.6s)
  • 配置资源预加载策略(preload/prefetch),关键资源并行加载,TTI 降至 1.2s
  • 优化图片资源:WebP 格式 + CDN + 懒加载,LCP 提升 70%(3.0s → 0.9s)
  • 建立性能监控体系,实时追踪 Core Web Vitals,性能劣化自动告警

模板三:Vite 项目优化

优化 Vite 生产构建产物,通过 Rollup 配置优化、依赖预构建、按需加载等手段,将构建产物从 4.2MB 压缩至 280KB(减少 93%),首屏加载时间从 3.8s 降至 0.9s。

  • 配置 Rollup manualChunks 精细化代码分割,提取公共依赖 vendor.js
  • 使用 vite-plugin-compression 启用 Gzip/Brotli 压缩,传输体积减少 85%
  • 优化依赖预构建(optimizeDeps),开发环境冷启动从 8s 降至 1.5s
  • 配置 Legacy 插件支持旧浏览器,同时保持现代浏览器的极致性能

面试话术模板

一、打包资源体积优化(5MB → 300KB)

场景一:面试官问"你们的包体积优化是怎么做的?"

【核心回答框架】

"我采用'分析 → 拆分 → 替换 → 压缩'的四步优化法,每一步都有明确的数据指标。"

背景(Situation): 项目上线后用户反馈加载慢,通过性能分析发现:

  • 包体积过大:生产环境 main.js 5.2MB,首屏加载 4.5s
  • 用户流失:首页跳出率 45%,移动端用户抱怨流量消耗大
  • 业务影响:转化率比竞品低 30%

任务(Task): 将包体积压缩到 500KB 以内,首屏加载降至 2s 以内。

行动(Action)

第一阶段:分析诊断

webpack-bundle-analyzer 体积分析

javascript
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
      openAnalyzer: true,
      generateStatsFile: true,
      statsFilename: 'bundle-stats.json',
      statsOptions: {
        source: false,
        reasons: false,
        warnings: false,
        errors: false,
        optimizationBailout: false
      },
      excludeAssets: /\.(map|txt)$/,
      logLevel: 'info'
    })
  ]
};

分析报告发现

plain
bundle.js (5.2MB)
├── node_modules (3.8MB - 73%)
│   ├── moment (500KB) ← 包含所有语言包
│   ├── lodash (350KB) ← 全量引入
│   ├── antd (1.2MB) ← 未按需加载
│   ├── echarts (800KB) ← 全量引入
│   └── 其他依赖 (950KB)
├── src (1.2MB - 23%)
│   ├── pages (800KB)
│   └── components (400KB)
└── assets (200KB - 4%)
    ├── images (150KB)
    └── fonts (50KB)

source-map-explorer 源码分析

bash
# 安装
npm install --save-dev source-map-explorer

# 生成 source map
npm run build

# 分析
npx source-map-explorer 'dist/js/*.js'
javascript
// package.json
{
  "scripts": {
    "analyze": "source-map-explorer 'dist/**/*.js' --html analysis.html"
  }
}

发现问题

plain
main.js 源码占比:
├── moment.js: 500KB (包含 locale/)
├── lodash: 350KB (全量导入)
├── node_modules/antd: 1.2MB
│   ├── es/button: 80KB
│   ├── es/table: 200KB
│   ├── es/form: 150KB
│   └── ... (大量未使用组件)
└── 业务代码: 1.2MB

识别大型依赖库

javascript
// analyze-deps.js
const fs = require('fs');
const path = require('path');

function getDirSize(dirPath) {
  let size = 0;
  const files = fs.readdirSync(dirPath);

  files.forEach(file => {
    const filePath = path.join(dirPath, file);
    const stats = fs.statSync(filePath);

    if (stats.isDirectory()) {
      size += getDirSize(filePath);
    } else {
      size += stats.size;
    }
  });

  return size;
}

const nodeModulesPath = path.join(__dirname, 'node_modules');
const packages = fs.readdirSync(nodeModulesPath);

const packageSizes = packages
  .filter(pkg => !pkg.startsWith('.'))
  .map(pkg => ({
    name: pkg,
    size: getDirSize(path.join(nodeModulesPath, pkg))
  }))
  .sort((a, b) => b.size - a.size);

console.log('📦 Top 20 最大的依赖包:\n');
packageSizes.slice(0, 20).forEach((pkg, i) => {
  console.log(`${i + 1}. ${pkg.name}: ${(pkg.size / 1024 / 1024).toFixed(2)}MB`);
});

分析结果

plain
Top 10 最大依赖:
1. moment: 15.2MB (包含源码 + locale)
2. antd: 12.8MB
3. echarts: 8.5MB
4. lodash: 5.2MB
5. react-dom: 3.8MB
6. @babel/runtime: 2.5MB
7. core-js: 2.1MB
8. axios: 1.8MB
9. react-router-dom: 1.5MB
10. dayjs: 0.5MB

重复依赖检测

bash
# 使用 npm
npm dedupe

# 使用 yarn
yarn dedupe

# 检查重复依赖
npx npm-check-duplicates
javascript
// check-duplicates.js
const fs = require('fs');
const path = require('path');

function findDuplicates(dir, packages = {}, depth = 0) {
  if (depth > 5) return packages; // 防止递归过深

  const nodeModules = path.join(dir, 'node_modules');
  if (!fs.existsSync(nodeModules)) return packages;

  const items = fs.readdirSync(nodeModules);

  items.forEach(item => {
    if (item.startsWith('.')) return;

    const pkgPath = path.join(nodeModules, item, 'package.json');
    if (fs.existsSync(pkgPath)) {
      const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
      const key = pkg.name;
      const version = pkg.version;

      if (!packages[key]) {
        packages[key] = new Set();
      }
      packages[key].add(version);

      // 递归检查嵌套依赖
      findDuplicates(path.join(nodeModules, item), packages, depth + 1);
    }
  });

  return packages;
}

const duplicates = findDuplicates(__dirname);

console.log('🔍 重复依赖检测:\n');
Object.entries(duplicates)
  .filter(([_, versions]) => versions.size > 1)
  .forEach(([name, versions]) => {
    console.log(`⚠️  ${name}: ${Array.from(versions).join(', ')}`);
  });

发现重复

plain
react: 17.0.2, 18.2.0
lodash: 4.17.20, 4.17.21
moment: 2.29.1, 2.29.4

第二阶段:代码分割策略

路由懒加载

React 路由懒加载

javascript
// ❌ 优化前:同步加载
import Dashboard from './pages/Dashboard';
import UserList from './pages/UserList';
import ProductList from './pages/ProductList';
import Settings from './pages/Settings';

const routes = [
  { path: '/dashboard', component: Dashboard },
  { path: '/users', component: UserList },
  { path: '/products', component: ProductList },
  { path: '/settings', component: Settings }
];

// ✅ 优化后:懒加载
import { lazy, Suspense } from 'react';
import Loading from './components/Loading';

const Dashboard = lazy(() => import(/* webpackChunkName: "dashboard" */ './pages/Dashboard'));
const UserList = lazy(() => import(/* webpackChunkName: "users" */ './pages/UserList'));
const ProductList = lazy(() => import(/* webpackChunkName: "products" */ './pages/ProductList'));
const Settings = lazy(() => import(/* webpackChunkName: "settings" */ './pages/Settings'));

const routes = [
  {
    path: '/dashboard',
    component: () => (
      <Suspense fallback={<Loading />}>
        <Dashboard />
      </Suspense>
    )
  },
  // ... 其他路由
];

Vue 路由懒加载

javascript
// ❌ 优化前
import Dashboard from './pages/Dashboard.vue';
import UserList from './pages/UserList.vue';

// ✅ 优化后
const routes = [
  {
    path: '/dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ './pages/Dashboard.vue')
  },
  {
    path: '/users',
    component: () => import(/* webpackChunkName: "users" */ './pages/UserList.vue')
  }
];

// Vue 3 defineAsyncComponent
import { defineAsyncComponent } from 'vue';

const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./pages/Dashboard.vue'),
  loadingComponent: Loading,
  errorComponent: Error,
  delay: 200,
  timeout: 3000
});

预加载关键路由

javascript
// 路由配置
const routes = [
  {
    path: '/dashboard',
    component: lazy(() => import('./pages/Dashboard')),
    preload: true  // 标记为需要预加载
  },
  {
    path: '/users',
    component: lazy(() => import('./pages/UserList'))
  }
];

// 预加载函数
function preloadRoute(route) {
  if (route.preload) {
    // 预加载但不执行
    const Component = route.component;
    if (typeof Component === 'function') {
      Component();
    }
  }
}

// 应用启动时预加载
routes.forEach(preloadRoute);

// 或者鼠标悬停时预加载
<Link
  to="/users"
  onMouseEnter={() => {
    import('./pages/UserList');
  }}
>
  用户管理
</Link>

组件懒加载

javascript
// 动态 import()
const HeavyComponent = lazy(() => import('./components/HeavyChart'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>显示图表</button>

      {showChart && (
        <Suspense fallback={<Skeleton />}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}

// 按需加载第三方组件
const EChartsComponent = lazy(() =>
  import('echarts-for-react').then(module => ({
    default: module.default
  }))
);

// 骨架屏占位
function ChartSkeleton() {
  return (
    <div className="skeleton">
      <div className="skeleton-header" />
      <div className="skeleton-body" />
    </div>
  );
}

SplitChunks 优化配置

javascript
// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,

      cacheGroups: {
        // React 核心库
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
          name: 'react-vendor',
          priority: 30,
          reuseExistingChunk: true
        },

        // UI 组件库
        ui: {
          test: /[\\/]node_modules[\\/](antd|@ant-design)[\\/]/,
          name: 'ui-vendor',
          priority: 20,
          reuseExistingChunk: true
        },

        // 工具库
        utils: {
          test: /[\\/]node_modules[\\/](lodash|moment|dayjs|axios)[\\/]/,
          name: 'utils-vendor',
          priority: 15,
          reuseExistingChunk: true
        },

        // 图表库
        charts: {
          test: /[\\/]node_modules[\\/](echarts|recharts|chart\.js)[\\/]/,
          name: 'charts-vendor',
          priority: 10,
          reuseExistingChunk: true
        },

        // 其他第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 5,
          reuseExistingChunk: true
        },

        // 公共代码
        common: {
          minChunks: 2,
          priority: 1,
          reuseExistingChunk: true,
          name: 'common'
        }
      }
    },

    // 运行时代码单独提取
    runtimeChunk: {
      name: entrypoint => `runtime-${entrypoint.name}`
    }
  }
};

效果对比

plain
优化前:
├── main.js: 5.2MB (所有代码)

优化后:
├── runtime.js: 2KB
├── react-vendor.js: 150KB
├── ui-vendor.js: 250KB
├── charts-vendor.js: 180KB (按需加载)
├── utils-vendor.js: 80KB
├── common.js: 50KB
├── dashboard.chunk.js: 120KB (首屏)
├── users.chunk.js: 100KB (懒加载)
├── products.chunk.js: 90KB (懒加载)
└── settings.chunk.js: 60KB (懒加载)

首屏加载: 150KB + 250KB + 80KB + 50KB + 120KB = 650KB

第三阶段:依赖优化

替换体积大的库

moment → dayjs
javascript
// ❌ 优化前 (500KB)
import moment from 'moment';
import 'moment/locale/zh-cn';

const date = moment().format('YYYY-MM-DD');

// ✅ 优化后 (7KB)
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import relativeTime from 'dayjs/plugin/relativeTime';

dayjs.extend(relativeTime);
dayjs.locale('zh-cn');

const date = dayjs().format('YYYY-MM-DD');
lodash → lodash-es + tree-shaking
javascript
// ❌ 优化前 (350KB - 全量引入)
import _ from 'lodash';

_.debounce(fn, 300);
_.throttle(fn, 1000);

// ✅ 优化后 (10KB - 按需引入)
import { debounce, throttle } from 'lodash-es';

debounce(fn, 300);
throttle(fn, 1000);

// 或使用原生实现
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
antd 按需加载
javascript
// ❌ 优化前 (1.2MB)
import { Button, Table, Form, Modal } from 'antd';
import 'antd/dist/reset.css';

// ✅ 优化后 - 方案1: babel-plugin-import
// .babelrc
{
  "plugins": [
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": "css"
    }]
  ]
}

// ✅ 优化后 - 方案2: 手动按需引入
import Button from 'antd/es/button';
import Table from 'antd/es/table';
import 'antd/es/button/style/css';
import 'antd/es/table/style/css';
echarts 按需引入
javascript
// ❌ 优化前 (800KB)
import * as echarts from 'echarts';

// ✅ 优化后 (200KB)
import * as echarts from 'echarts/core';
import { BarChart, LineChart, PieChart } from 'echarts/charts';
import {
  GridComponent,
  TooltipComponent,
  LegendComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';

echarts.use([
  BarChart,
  LineChart,
  PieChart,
  GridComponent,
  TooltipComponent,
  LegendComponent,
  CanvasRenderer
]);

体积对比

优化前 优化后 减少
moment → dayjs 500KB 7KB -98.6%
lodash → lodash-es 350KB 10KB -97.1%
antd 按需加载 1.2MB 300KB -75%
echarts 按需引入 800KB 200KB -75%
总计 2.85MB 517KB -81.9%

Tree Shaking 优化

javascript
// package.json
{
  "sideEffects": [
    "*.css",
    "*.scss",
    "*.less"
  ]
}

// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,  // 标记未使用的导出
    sideEffects: true,  // 删除无副作用的模块
    minimize: true
  }
};

// 确保使用 ESM 格式
// ❌ CommonJS (无法 Tree Shaking)
const utils = require('./utils');

// ✅ ESM (可以 Tree Shaking)
import { add, subtract } from './utils';

验证 Tree Shaking

javascript
// utils.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export function multiply(a, b) {  // 未使用
  return a * b;
}

// main.js
import { add } from './utils';  // 只导入 add

console.log(add(1, 2));

// 构建后 multiply 函数会被删除

externals 外部化

javascript
// webpack.config.js
module.exports = {
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
    'vue': 'Vue',
    'axios': 'axios',
    'lodash': '_',
    'moment': 'moment'
  }
};

// index.html
<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <div id="root"></div>

  <!-- CDN 引入 -->
  <script crossorigin src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios@1/dist/axios.min.js"></script>

  <!-- 应用代码 -->
  <script src="/static/js/main.js"></script>
</body>
</html>

第四阶段:代码压缩

JS 压缩

javascript
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true,
            pure_funcs: ['console.log', 'console.info'],
            passes: 2
          },
          format: {
            comments: false
          },
          mangle: {
            safari10: true
          }
        },
        extractComments: false
      })
    ]
  }
};

CSS 压缩

javascript
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin');
const glob = require('glob');
const path = require('path');

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css'
    }),

    // PurgeCSS 移除未使用的 CSS
    new PurgeCSSPlugin({
      paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, {
        nodir: true
      }),
      safelist: {
        standard: [/^ant-/, /^rc-/],  // 保留 antd 样式
        deep: [/^ant-/, /^rc-/],
        greedy: [/data-theme$/]
      }
    })
  ],

  optimization: {
    minimizer: [
      new CssMinimizerPlugin({
        minimizerOptions: {
          preset: [
            'default',
            {
              discardComments: { removeAll: true }
            }
          ]
        }
      })
    ]
  }
};

图片压缩

javascript
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024  // 10KB 以下内联
          }
        },
        use: [
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                progressive: true,
                quality: 75
              },
              optipng: {
                enabled: true
              },
              pngquant: {
                quality: [0.65, 0.90],
                speed: 4
              },
              gifsicle: {
                interlaced: false
              },
              webp: {
                quality: 75
              }
            }
          }
        ]
      }
    ]
  }
};

第五阶段:高级优化

Gzip/Brotli 压缩

javascript
// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [
    // Gzip
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,
      minRatio: 0.8
    }),

    // Brotli (更高压缩率)
    new CompressionPlugin({
      filename: '[path][base].br',
      algorithm: 'brotliCompress',
      test: /\.(js|css|html|svg)$/,
      compressionOptions: {
        level: 11
      },
      threshold: 10240,
      minRatio: 0.8
    })
  ]
};

Nginx 配置

nginx

nginx
# gzip 配置
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;

# brotli 配置
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml+xml application/xml+rss text/javascript;

Scope Hoisting

javascript
// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ],

  optimization: {
    concatenateModules: true
  }
};

结果(Result)
指标 优化前 优化后 提升
总体积 5.2MB 300KB -94.2%
首屏资源 5.2MB 150KB -97.1%
Gzip 后 1.8MB 80KB -95.5%
FCP 2.5s 0.8s -68%
LCP 4.5s 1.2s -73.3%

二、白屏时间优化(3s → <1s)

场景一:面试官问"白屏时间优化怎么做?"

【核心回答框架】

"我从资源加载、首屏渲染、代码执行三个维度系统优化,将 FCP 从 2.5s 降至 0.6s。"

背景(Situation): 用户反馈页面加载慢,白屏时间长,通过 Lighthouse 检测发现:

  • FCP (首次内容绘制): 2.5s
  • LCP (最大内容绘制): 3.0s
  • TTI (可交互时间): 4.5s
  • 性能评分: 45 分(C级)

任务(Task): 将白屏时间降至 1s 以内,性能评分提升至 90+ 分。

行动(Action)

第一步:性能指标监控

Performance API 采集

javascript
// performance-monitor.js
class PerformanceMonitor {
  constructor() {
    this.metrics = {};
  }

  // 采集 Core Web Vitals
  collectMetrics() {
    // FCP - 首次内容绘制
    const fcp = performance.getEntriesByName('first-contentful-paint')[0];
    if (fcp) {
      this.metrics.fcp = fcp.startTime;
    }

    // LCP - 最大内容绘制
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.metrics.lcp = lastEntry.renderTime || lastEntry.loadTime;
    }).observe({ entryTypes: ['largest-contentful-paint'] });

    // FID - 首次输入延迟
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      entries.forEach(entry => {
        this.metrics.fid = entry.processingStart - entry.startTime;
      });
    }).observe({ entryTypes: ['first-input'] });

    // CLS - 累积布局偏移
    let clsScore = 0;
    new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        if (!entry.hadRecentInput) {
          clsScore += entry.value;
        }
      });
      this.metrics.cls = clsScore;
    }).observe({ entryTypes: ['layout-shift'] });

    // TTI - 可交互时间
    if (performance.getEntriesByType) {
      const navigationEntry = performance.getEntriesByType('navigation')[0];
      if (navigationEntry) {
        this.metrics.tti = navigationEntry.domInteractive;
        this.metrics.domContentLoaded = navigationEntry.domContentLoadedEventEnd;
        this.metrics.loadComplete = navigationEntry.loadEventEnd;
      }
    }

    // 资源加载时间
    this.collectResourceTiming();
  }

  collectResourceTiming() {
    const resources = performance.getEntriesByType('resource');

    this.metrics.resources = {
      total: resources.length,
      js: resources.filter(r => r.name.endsWith('.js')).length,
      css: resources.filter(r => r.name.endsWith('.css')).length,
      images: resources.filter(r => /\.(png|jpg|jpeg|gif|svg|webp)$/.test(r.name)).length,
      totalSize: resources.reduce((sum, r) => sum + (r.transferSize || 0), 0)
    };

    // 慢资源
    this.metrics.slowResources = resources
      .filter(r => r.duration > 1000)
      .map(r => ({
        name: r.name,
        duration: r.duration,
        size: r.transferSize
      }));
  }

  // 上报数据
  report() {
    // 延迟上报,确保所有指标采集完成
    setTimeout(() => {
      console.log('📊 性能指标:', this.metrics);

      // 上报到服务器
      navigator.sendBeacon('/api/performance', JSON.stringify(this.metrics));
    }, 3000);
  }

  // 评分
  getScore() {
    let score = 100;

    // FCP 评分
    if (this.metrics.fcp > 3000) score -= 20;
    else if (this.metrics.fcp > 1800) score -= 10;

    // LCP 评分
    if (this.metrics.lcp > 4000) score -= 20;
    else if (this.metrics.lcp > 2500) score -= 10;

    // FID 评分
    if (this.metrics.fid > 300) score -= 15;
    else if (this.metrics.fid > 100) score -= 5;

    // CLS 评分
    if (this.metrics.cls > 0.25) score -= 15;
    else if (this.metrics.cls > 0.1) score -= 5;

    return Math.max(0, score);
  }
}

// 使用
const monitor = new PerformanceMonitor();

window.addEventListener('load', () => {
  monitor.collectMetrics();
  monitor.report();

  console.log('性能评分:', monitor.getScore());
});

第二步:资源加载优化

Critical CSS 内联

javascript
// build-critical-css.js
const critical = require('critical');
const path = require('path');

critical.generate({
  inline: true,
  base: 'dist/',
  src: 'index.html',
  target: {
    html: 'index.html',
    css: 'critical.css'
  },
  width: 1300,
  height: 900,
  dimensions: [
    { width: 375, height: 667 },   // Mobile
    { width: 1920, height: 1080 }  // Desktop
  ],
  penthouse: {
    blockJSRequests: false
  }
});

Webpack 插件方式

javascript

javascript
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlCriticalWebpackPlugin = require('html-critical-webpack-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html'
    }),
    new HtmlCriticalWebpackPlugin({
      base: path.resolve(__dirname, 'dist'),
      src: 'index.html',
      dest: 'index.html',
      inline: true,
      minify: true,
      extract: true,
      width: 375,
      height: 565,
      penthouse: {
        blockJSRequests: false
      }
    })
  ]
};

资源预加载策略

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>My App</title>

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

  <!-- 预连接 -->
  <link rel="preconnect" href="https://api.example.com">
  <link rel="preconnect" href="https://cdn.example.com">

  <!-- 预加载关键资源 -->
  <link rel="preload" href="/static/js/vendor.js" as="script">
  <link rel="preload" href="/static/css/main.css" as="style">
  <link rel="preload" href="/static/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

  <!-- 预取下一页资源 -->
  <link rel="prefetch" href="/static/js/dashboard.chunk.js">

  <!-- Critical CSS 内联 -->
  <style>
    /* 首屏关键 CSS */
    body { margin: 0; font-family: sans-serif; }
    .loading { display: flex; justify-content: center; }
  </style>

  <!-- 非关键 CSS 异步加载 -->
  <link rel="preload" href="/static/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/static/css/main.css"></noscript>
</head>
<body>
  <div id="root">
    <!-- 骨架屏 -->
    <div class="skeleton">
      <div class="skeleton-header"></div>
      <div class="skeleton-content"></div>
    </div>
  </div>

  <!-- 关键 JS 同步加载 -->
  <script src="/static/js/runtime.js"></script>
  <script src="/static/js/vendor.js"></script>

  <!-- 应用 JS 延迟加载 -->
  <script src="/static/js/main.js" defer></script>

  <!-- 非关键 JS 异步加载 -->
  <script src="/static/js/analytics.js" async></script>
</body>
</html>

动态资源加载

javascript
// resource-loader.js
class ResourceLoader {
  constructor() {
    this.loadedResources = new Set();
  }

  // 预加载脚本
  preloadScript(src) {
    if (this.loadedResources.has(src)) return Promise.resolve();

    return new Promise((resolve, reject) => {
      const link = document.createElement('link');
      link.rel = 'preload';
      link.as = 'script';
      link.href = src;
      link.onload = resolve;
      link.onerror = reject;
      document.head.appendChild(link);

      this.loadedResources.add(src);
    });
  }

  // 加载脚本
  loadScript(src) {
    if (this.loadedResources.has(src)) return Promise.resolve();

    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);

      this.loadedResources.add(src);
    });
  }

  // 加载样式
  loadStyle(href) {
    if (this.loadedResources.has(href)) return Promise.resolve();

    return new Promise((resolve, reject) => {
      const link = document.createElement('link');
      link.rel = 'stylesheet';
      link.href = href;
      link.onload = resolve;
      link.onerror = reject;
      document.head.appendChild(link);

      this.loadedResources.add(href);
    });
  }

  // 预加载图片
  preloadImage(src) {
    if (this.loadedResources.has(src)) return Promise.resolve();

    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = resolve;
      img.onerror = reject;
      img.src = src;

      this.loadedResources.add(src);
    });
  }
}

// 使用
const loader = new ResourceLoader();

// 预加载下一页资源
document.querySelector('a[href="/dashboard"]').addEventListener('mouseenter', () => {
  loader.preloadScript('/static/js/dashboard.chunk.js');
});

第三步:首屏渲染优化

SSR 服务端渲染(React)

javascript
// server.js
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const { StaticRouter } = require('react-router-dom');
const App = require('./App').default;
const fs = require('fs');
const path = require('path');

const app = express();
const PORT = 3000;

// 读取客户端构建的 HTML 模板
const template = fs.readFileSync(
  path.resolve(__dirname, '../dist/index.html'),
  'utf-8'
);

// 提取关键 CSS
const criticalCSS = `<style>${fs.readFileSync(
  path.resolve(__dirname, '../dist/critical.css'),
  'utf-8'
)}</style>`;

app.use('/static', express.static(path.resolve(__dirname, '../dist/static')));

app.get('*', (req, res) => {
  const context = {};

  // 渲染 React 组件为 HTML 字符串
  const html = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );

  // 如果有重定向
  if (context.url) {
    return res.redirect(301, context.url);
  }

  // 注入到 HTML 模板
  const finalHTML = template
    .replace('<!-- critical-css -->', criticalCSS)
    .replace('<div id="root"></div>', `<div id="root">${html}</div>`)
    .replace('<!-- preload-state -->', `<script>window.__INITIAL_STATE__=${JSON.stringify({})}</script>`);

  res.send(finalHTML);
});

app.listen(PORT, () => {
  console.log(`SSR 服务器运行在 http://localhost:${PORT}`);
});

客户端 Hydration

javascript

javascript
// client.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

const root = ReactDOM.hydrateRoot(
  document.getElementById('root'),
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

SSG 静态生成(Next.js)

javascript

javascript
// pages/index.js
export default function Home({ posts }) {
  return (
    <div>
      <h1>博客</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

// 静态生成
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

  return {
    props: { posts },
    revalidate: 60  // ISR: 60秒后重新生成
  };
}

骨架屏占位

javascript

javascript
// Skeleton.jsx
function Skeleton() {
  return (
    <div className="skeleton">
      <div className="skeleton-header">
        <div className="skeleton-avatar"></div>
        <div className="skeleton-title"></div>
      </div>
      <div className="skeleton-content">
        <div className="skeleton-line"></div>
        <div className="skeleton-line"></div>
        <div className="skeleton-line short"></div>
      </div>
    </div>
  );
}

// App.jsx
import { Suspense, lazy } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <Dashboard />
    </Suspense>
  );
}

CSS

css

css
.skeleton {
  padding: 20px;
}

.skeleton-header {
  display: flex;
  align-items: center;
  margin-bottom: 20px;
}

.skeleton-avatar {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: skeleton-loading 1.5s infinite;
}

.skeleton-title {
  flex: 1;
  height: 20px;
  margin-left: 15px;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: skeleton-loading 1.5s infinite;
}

.skeleton-line {
  height: 16px;
  margin-bottom: 10px;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: skeleton-loading 1.5s infinite;
}

.skeleton-line.short {
  width: 60%;
}

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

渐进式渲染

javascript

javascript
// progressive-render.js
function ProgressiveRender({ children }) {
  const [visibleItems, setVisibleItems] = useState(10);
  const containerRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          // 加载更多
          setVisibleItems(prev => Math.min(prev + 10, children.length));
        }
      },
      { threshold: 0.5 }
    );

    const sentinel = containerRef.current?.querySelector('.sentinel');
    if (sentinel) {
      observer.observe(sentinel);
    }

    return () => observer.disconnect();
  }, [visibleItems, children.length]);

  return (
    <div ref={containerRef}>
      {children.slice(0, visibleItems)}
      {visibleItems < children.length && (
        <div className="sentinel" style={{ height: '1px' }} />
      )}
    </div>
  );
}

第四步:静态资源优化

CDN 加速

javascript

javascript
// webpack.config.js
const CDN_URL = process.env.CDN_URL || '';

module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js',
    publicPath: CDN_URL + '/'
  }
};

CDN 配置策略

javascript

javascript
// cdn-config.js
const CDN_DOMAINS = {
  static: 'https://static.example.com',
  images: 'https://img.example.com',
  videos: 'https://video.example.com'
};

function getCDNUrl(type, path) {
  const domain = CDN_DOMAINS[type] || CDN_DOMAINS.static;
  return `${domain}${path}`;
}

// 使用
<img src={getCDNUrl('images', '/logo.png')} alt="Logo" />

HTTP/2 多路复用

Nginx 配置

nginx

nginx
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # HTTP/2 推送
    location / {
        root /var/www/html;
        index index.html;

        # 推送关键资源
        http2_push /static/css/main.css;
        http2_push /static/js/vendor.js;
        http2_push /static/js/main.js;
    }
}

资源长缓存

javascript

javascript
// webpack.config.js
module.exports = {
  output: {
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].chunk.js'
  },

  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css'
    })
  ]
};

Nginx 缓存配置

nginx

nginx
location ~* \.(js|css)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

location ~* \.(woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

location / {
    expires -1;
    add_header Cache-Control "no-cache";
}

图片优化

javascript

javascript
// 响应式图片
<picture>
  <source
    srcset="/images/hero.webp"
    type="image/webp"
  />
  <source
    srcset="/images/hero.jpg"
    type="image/jpeg"
  />
  <img
    src="/images/hero.jpg"
    alt="Hero"
    loading="lazy"
    decoding="async"
  />
</picture>

// 不同尺寸
<img
  srcset="
    /images/small.jpg 400w,
    /images/medium.jpg 800w,
    /images/large.jpg 1200w
  "
  sizes="
    (max-width: 400px) 400px,
    (max-width: 800px) 800px,
    1200px
  "
  src="/images/medium.jpg"
  alt="Responsive"
  loading="lazy"
/>

第五步:代码执行优化

延迟加载非关键 JS

html
<!-- 关键 JS 立即执行 -->
<script src="/runtime.js"></script>
<script src="/vendor.js"></script>
<script src="/main.js"></script>

<!-- 非关键 JS 延迟执行 -->
<script src="/analytics.js" defer></script>
<script src="/chat-widget.js" defer></script>

<!-- 异步加载 -->
<script src="/ads.js" async></script>

requestIdleCallback 空闲执行

javascript

javascript
// idle-task.js
function runWhenIdle(task, options = {}) {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(task, options);
  } else {
    // 降级方案
    setTimeout(task, 1);
  }
}

// 使用
runWhenIdle(() => {
  // 非关键任务
  initAnalytics();
  loadChatWidget();
  preloadNextPage();
}, { timeout: 2000 });

Web Worker 后台计算

javascript

javascript
// worker.js
self.addEventListener('message', (e) => {
  const { type, data } = e.data;

  if (type === 'HEAVY_CALCULATION') {
    const result = heavyCalculation(data);
    self.postMessage({ type: 'RESULT', data: result });
  }
});

function heavyCalculation(data) {
  // 耗时计算
  return data.map(item => {
    // 复杂运算
    return processItem(item);
  });
}

// main.js
const worker = new Worker('/worker.js');

worker.postMessage({
  type: 'HEAVY_CALCULATION',
  data: largeDataSet
});

worker.addEventListener('message', (e) => {
  const { type, data } = e.data;
  if (type === 'RESULT') {
    console.log('计算结果:', data);
  }
});

虚拟列表优化

javascript

javascript
// VirtualList.jsx
import { useEffect, useRef, useState } from 'react';

function VirtualList({ items, itemHeight, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);

  // 可见区域
  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;

  const handleScroll = (e) => {
    setScrollTop(e.target.scrollTop);
  };

  return (
    <div
      ref={containerRef}
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div
              key={startIndex + index}
              style={{ height: itemHeight }}
            >
              {item.content}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

第六步:缓存策略

Service Worker 离线缓存

javascript

javascript
// sw.js
const CACHE_NAME = 'v1.0.0';
const urlsToCache = [
  '/',
  '/static/css/main.css',
  '/static/js/vendor.js',
  '/static/js/main.js'
];

// 安装
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

// 激活
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== CACHE_NAME) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

// 拦截请求
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // 缓存命中
        if (response) {
          return response;
        }

        // 网络请求
        return fetch(event.request).then(response => {
          // 缓存新资源
          if (event.request.method === 'GET') {
            const responseToCache = response.clone();
            caches.open(CACHE_NAME).then(cache => {
              cache.put(event.request, responseToCache);
            });
          }

          return response;
        });
      })
  );
});

注册 Service Worker

javascript

javascript
// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log('SW 注册成功:', registration);
      })
      .catch(error => {
        console.log('SW 注册失败:', error);
      });
  });
}

三、Vite 项目优化

场景一:面试官问"Vite 项目怎么优化?"

【核心回答】

"Vite 虽然开发快,但生产构建也需要优化,主要从 Rollup 配置和依赖预构建入手。"

Rollup manualChunks 代码分割

javascript

javascript
// vite.config.js
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 手动分包
        manualChunks: {
          // React 生态
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],

          // UI 组件库
          'ui-vendor': ['antd', '@ant-design/icons'],

          // 工具库
          'utils-vendor': ['dayjs', 'axios', 'lodash-es'],

          // 图表库
          'charts-vendor': ['echarts', 'echarts-for-react']
        },

        // 自定义分包逻辑
        manualChunks(id) {
          // node_modules 按包名分割
          if (id.includes('node_modules')) {
            const match = id.match(/node_modules\/(.+?)\//);
            if (match) {
              const packageName = match[1];

              // 大型库单独分包
              if (['antd', 'echarts', '@ant-design'].some(name => packageName.includes(name))) {
                return `vendor-${packageName.replace('@', '')}`;
              }
            }
            return 'vendor';
          }
        },

        // 文件名格式
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: '[ext]/[name]-[hash].[ext]'
      }
    },

    // chunk 大小警告阈值
    chunkSizeWarningLimit: 1000,

    // 压缩选项
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    },

    // CSS 代码分割
    cssCodeSplit: true,

    // Source Map
    sourcemap: false
  },

  plugins: [
    // 打包分析
    visualizer({
      filename: 'dist/stats.html',
      open: true,
      gzipSize: true,
      brotliSize: true
    })
  ]
});

依赖预构建优化

javascript

javascript
// vite.config.js
export default defineConfig({
  optimizeDeps: {
    // 预构建依赖
    include: [
      'react',
      'react-dom',
      'react-router-dom',
      'antd',
      'dayjs',
      'axios',
      'lodash-es'
    ],

    // 排除预构建
    exclude: [
      '@vite/client',
      '@vite/env'
    ],

    // 强制预构建
    force: false,

    // ESBuild 选项
    esbuildOptions: {
      target: 'es2015',
      supported: {
        'top-level-await': true
      }
    }
  }
});

Vite 插件优化

javascript

javascript
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import compression from 'vite-plugin-compression';
import { createHtmlPlugin } from 'vite-plugin-html';
import legacy from '@vitejs/plugin-legacy';
import viteImagemin from 'vite-plugin-imagemin';

export default defineConfig({
  plugins: [
    react(),

    // Gzip/Brotli 压缩
    compression({
      algorithm: 'gzip',
      ext: '.gz',
      threshold: 10240,
      deleteOriginFile: false
    }),
    compression({
      algorithm: 'brotliCompress',
      ext: '.br',
      threshold: 10240,
      deleteOriginFile: false
    }),

    // HTML 注入
    createHtmlPlugin({
      minify: true,
      inject: {
        data: {
          title: 'My App',
          injectScript: '<script src="/config.js"></script>'
        }
      }
    }),

    // 兼容旧浏览器
    legacy({
      targets: ['defaults', 'not IE 11'],
      additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
      renderLegacyChunks: true,
      polyfills: [
        'es.symbol',
        'es.array.filter',
        'es.promise',
        'es.promise.finally'
      ]
    }),

    // 图片压缩
    viteImagemin({
      gifsicle: {
        optimizationLevel: 7,
        interlaced: false
      },
      optipng: {
        optimizationLevel: 7
      },
      mozjpeg: {
        quality: 80
      },
      pngquant: {
        quality: [0.8, 0.9],
        speed: 4
      },
      svgo: {
        plugins: [
          { name: 'removeViewBox' },
          { name: 'removeEmptyAttrs', active: false }
        ]
      }
    })
  ]
});

Vite 开发环境优化

javascript

javascript
// vite.config.js
export default defineConfig({
  server: {
    // 预热常用文件
    warmup: {
      clientFiles: [
        './src/pages/**/*.tsx',
        './src/components/**/*.tsx'
      ]
    },

    // 端口
    port: 3000,

    // 自动打开浏览器
    open: true,

    // 代理
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    },

    // HMR
    hmr: {
      overlay: true
    }
  },

  // 预加载
  build: {
    modulePreload: {
      polyfill: true
    }
  }
});

动态导入与 Magic Comments

javascript

javascript
// 路由懒加载 + 预加载
const Dashboard = lazy(() =>
  import(
    /* webpackChunkName: "dashboard" */
    /* webpackPrefetch: true */
    './pages/Dashboard'
  )
);

// Vite 动态导入
const UserList = () => import('./pages/UserList');

// 按需导入 Antd
import { Button } from 'antd';  // Vite 自动 tree-shaking

// 图片优化
import imageSrc from './image.png?url';  // 获取 URL
import imageOpt from './image.png?w=400&h=300';  // 调整尺寸
import imageWebp from './image.png?format=webp';  // 转 WebP

最终优化效果对比

打包体积优化

plain
【优化前: 5.2MB】
├── main.js: 3.5MB
│   ├── moment: 500KB
│   ├── lodash: 350KB
│   ├── antd: 1.2MB
│   ├── echarts: 800KB
│   └── 业务代码: 650KB
├── main.css: 300KB
├── images: 1.2MB
└── fonts: 200KB

【优化后: 300KB (首屏)】
├── runtime.js: 2KB
├── react-vendor.js: 150KB (缓存)
├── ui-vendor.js: 80KB (按需)
├── utils-vendor.js: 20KB (dayjs)
├── common.js: 30KB
├── main.js: 100KB (首屏)
├── main.css: 50KB (Critical + Purge)
├── dashboard.chunk.js: 80KB (懒加载)
├── users.chunk.js: 60KB (懒加载)
└── images: 100KB (WebP + 懒加载)

【Gzip 后】
优化前: 1.8MB
优化后: 80KB (首屏)

【提升效果】
- 总体积: -94.2% (5.2MB → 300KB)
- 首屏资源: -97.1% (5.2MB → 150KB)
- Gzip 体积: -95.5% (1.8MB → 80KB)

白屏时间优化

plain
【优化前】
├── FCP: 2.5s
├── LCP: 3.0s
├── FID: 180ms
├── TTI: 4.5s
├── CLS: 0.15
└── 性能评分: 45 分

【优化后】
├── FCP: 0.6s (-76%) ← SSR + Critical CSS
├── LCP: 0.9s (-70%) ← 图片优化 + CDN
├── FID: 50ms (-72%) ← 代码分割
├── TTI: 1.2s (-73%) ← 懒加载
├── CLS: 0.05 (-67%) ← 骨架屏
└── 性能评分: 95 分 (+111%)

【分项优化贡献】
1. SSR 服务端渲染: FCP -1.2s
2. Critical CSS 内联: FCP -0.4s
3. 代码分割 (150KB): LCP -0.8s
4. 图片 WebP + CDN: LCP -0.6s
5. 预加载策略: TTI -1.5s
6. 骨架屏占位: CLS -0.1
7. Service Worker: 二次访问 FCP -0.5s

加载流程对比

优化前

plain
0s ─────────────────────────────────────────────────► 4.5s
│
├─ 0-1.0s: 下载 HTML
├─ 1.0-2.5s: 下载并解析 main.js (5.2MB)
├─ 2.5-3.0s: 下载并解析 CSS
├─ 3.0-3.5s: 执行 React 渲染
├─ 3.5-4.0s: 加载图片资源
└─ 4.5s: 页面可交互

白屏时间: 2.5s
用户看到内容: 3.5s
可交互: 4.5s

优化后

plain
0s ───────────────────────────────────────────► 1.2s
│
├─ 0-0.2s: 下载 HTML (含 SSR 内容 + Critical CSS)
├─ 0.2s: 用户看到首屏内容 (SSR)
├─ 0.2-0.6s: 下载 vendor.js (150KB, CDN)
├─ 0.6-0.8s: 下载 main.js (100KB)
├─ 0.8-1.0s: React Hydration
├─ 1.0-1.2s: 加载图片 (WebP, 懒加载)
└─ 1.2s: 页面可交互

白屏时间: 0.2s (SSR)
用户看到内容: 0.6s
可交互: 1.2s

用户体验提升

指标 优化前 优化后 提升
首屏加载时间 4.5s 1.2s -73.3%
白屏时间 2.5s 0.6s -76%
可交互时间 4.5s 1.2s -73.3%
页面跳出率 45% 12% -73.3%
用户满意度 65% 92% +41.5%
转化率 2.3% 4.8% +108.7%

面试高频追问

Q1: SSR 和 SSG 如何选择?

回答: 根据内容特点选择:

场景 推荐方案 原因
电商首页 SSR 内容实时变化,需要个性化
博客文章 SSG + ISR 内容静态,访问量大
用户中心 CSR 内容私密,需要登录
新闻列表 SSR 内容频繁更新
产品文档 SSG 内容稳定,SEO 重要

混合方案

javascript

javascript
// Next.js
export async function getServerSideProps() {
  // 动态页面使用 SSR
}

export async function getStaticProps() {
  return {
    props: {},
    revalidate: 60  // ISR: 60秒后重新生成
  }
}

Q2: 如何平衡首屏体积和用户体验?

回答: 遵循"关键渲染路径"原则:

  1. 必须同步加载(首屏 < 100KB)
    • HTML 结构
    • Critical CSS
    • 首屏 JS 核心逻辑
  2. 可以延迟加载
    • 非首屏组件
    • 图片、视频
    • 第三方脚本
  3. 预加载策略
    • 鼠标悬停预加载
    • 空闲时预取
    • 智能预测下一页

实战案例

javascript

javascript
// 首屏组件立即加载
import Header from './Header';

// 非首屏组件懒加载
const Footer = lazy(() => import('./Footer'));
const Sidebar = lazy(() => import('./Sidebar'));

// 预加载下一页
<Link
  to="/products"
  onMouseEnter={() => {
    import('./pages/Products');
  }}
>
  产品
</Link>

Q3: 图片优化的完整方案?

回答: 六个层面优化:

  1. 格式选择
    • WebP(体积减少 30%)
    • AVIF(更优,兼容性差)
    • SVG(矢量图)
  2. 尺寸适配
    • srcset 响应式
    • 按设备分辨率
  3. 压缩优化
    • 有损压缩(质量 75-80)
    • 无损压缩
  4. 加载策略
    • 懒加载(loading="lazy")
    • 渐进式加载
    • 占位符(LQIP/BlurHash)
  5. CDN 加速
    • 图片 CDN
    • 自动格式转换
    • 智能裁剪
  6. 缓存策略
    • 长缓存(1年)
    • 版本化 URL

Q4: Service Worker 有什么坑?

回答: 三个常见问题:

  1. 缓存更新问题

javascript

javascript
// 解决方案:版本化缓存名
   const CACHE_VERSION = 'v1.0.1';

   self.addEventListener('activate', (event) => {
     event.waitUntil(
       caches.keys().then(keys => {
         return Promise.all(
           keys.filter(key => key !== CACHE_VERSION)
             .map(key => caches.delete(key))
         );
       })
     );
   });
  1. 开发环境干扰

javascript

javascript
// 仅生产环境启用
   if (process.env.NODE_ENV === 'production') {
     navigator.serviceWorker.register('/sw.js');
   }
  1. 跨域问题

javascript

javascript
// 设置 CORS 模式
   fetch(url, {
     mode: 'cors',
     credentials: 'include'
   });

Q5: Vite 生产构建慢怎么办?

回答: 四个优化方向:

  1. 减少依赖

javascript

javascript
// 替换大型库
   moment → dayjs
   lodash → lodash-es
  1. 优化配置

javascript

javascript
build: {
     rollupOptions: {
       output: {
         manualChunks: { /* 精细分包 */ }
       }
     }
   }
  1. 并行压缩

javascript

javascript
build: {
     minify: 'esbuild',  // 比 terser 快 10 倍
     terserOptions: {
       compress: {
         drop_console: true
       }
     }
   }
  1. 缓存策略

bash

bash
# 使用构建缓存
   vite build --cache

简历亮点总结

打包体积优化

一句话总结

系统性优化前端资源体积,通过依赖替换、代码分割、Tree Shaking 等手段,将生产包从 5.2MB 压缩至 300KB(减少 94%),首屏资源从 5.2MB 降至 150KB,Gzip 后仅 80KB。

可量化指标

  • 包体积减少 94.2%(5.2MB → 300KB)
  • 首屏资源减少 97.1%(5.2MB → 150KB)
  • Gzip 体积减少 95.5%(1.8MB → 80KB)
  • FCP 提升 76%(2.5s → 0.6s)
  • 转化率提升 108.7%(2.3% → 4.8%)

白屏时间优化

一句话总结

实施 SSR + Critical CSS + 资源预加载策略,将首屏白屏时间从 3s 优化至 0.6s(提升 80%),Core Web Vitals 全部达到优秀标准,性能评分从 45 分提升至 95 分。

可量化指标

  • FCP 从 2.5s → 0.6s(提升 76%)
  • LCP 从 3.0s → 0.9s(提升 70%)
  • TTI 从 4.5s → 1.2s(提升 73%)
  • 性能评分从 45 → 95 分(+111%)
  • 用户满意度从 65% → 92%(+41.5%)
  • 页面跳出率从 45% → 12%(-73.3%)

Vite 项目优化

一句话总结

优化 Vite 生产构建配置,通过 Rollup manualChunks、依赖预构建、插件优化等手段,将构建产物从 4.2MB 压缩至 280KB(减少 93%),开发环境冷启动从 8s 降至 1.5s。

可量化指标

  • 构建产物减少 93%(4.2MB → 280KB)
  • 开发冷启动提升 81%(8s → 1.5s)
  • HMR 更新 < 50ms
  • 首屏 chunk < 150KB
  • CSS 体积减少 85%(PurgeCSS)

技术亮点

  • 精准分析: webpack-bundle-analyzer 识别瓶颈
  • 依赖替换: moment → dayjs(-98.6%)
  • 代码分割: 路由级懒加载覆盖率 100%
  • Tree Shaking: 移除 70% 未使用代码
  • 多重压缩: Gzip + Brotli + 图片优化
  • SSR渲染: FCP 提升 76%
  • 资源预加载: preload + prefetch 策略
  • 离线缓存: Service Worker 二次访问提速 80%# 打包体积与白屏时间优化 - 面试完整指南

简历描述模板(STAR法则)

模板一:打包体积优化(5MB → 300KB)

主导前端资源体积优化,通过代码分割、依赖替换、Tree Shaking 等手段,将生产环境包体积从 5MB 压缩至 300KB(减少 94%),首屏资源从 5MB 降至 150KB,首屏加载时间从 4.5s 降至 1.2s。

  • 使用 webpack-bundle-analyzer 深度分析,识别 moment.js、lodash、echarts 等大型依赖占用 3.8MB
  • 替换大型库:moment → dayjs(500KB → 7KB),lodash → lodash-es + tree-shaking(300KB → 50KB)
  • 实施路由级代码分割,首屏代码从 5MB 降至 150KB,懒加载覆盖率 100%
  • 配置 Gzip + CDN + 缓存策略,实际传输体积降至 80KB,缓存命中率 85%

模板二:白屏时间优化(3s → <1s)

系统性优化前端加载性能,从资源、渲染、缓存三个维度突破白屏瓶颈。将 FCP 从 2.5s 降至 0.6s,LCP 从 3.0s 降至 0.9s,用户体验评分从 C 级提升至 A+ 级。

  • 实施 SSR 服务端渲染 + Critical CSS 内联,FCP 提升 76%(2.5s → 0.6s)
  • 配置资源预加载策略(preload/prefetch),关键资源并行加载,TTI 降至 1.2s
  • 优化图片资源:WebP 格式 + CDN + 懒加载,LCP 提升 70%(3.0s → 0.9s)
  • 建立性能监控体系,实时追踪 Core Web Vitals,性能劣化自动告警

模板三:Vite 项目优化

优化 Vite 生产构建产物,通过 Rollup 配置优化、依赖预构建、按需加载等手段,将构建产物从 4.2MB 压缩至 280KB(减少 93%),首屏加载时间从 3.8s 降至 0.9s。

  • 配置 Rollup manualChunks 精细化代码分割,提取公共依赖 vendor.js
  • 使用 vite-plugin-compression 启用 Gzip/Brotli 压缩,传输体积减少 85%
  • 优化依赖预构建(optimizeDeps),开发环境冷启动从 8s 降至 1.5s
  • 配置 Legacy 插件支持旧浏览器,同时保持现代浏览器的极致性能

面试话术模板

一、打包资源体积优化(5MB → 300KB)

场景一:面试官问"你们的包体积优化是怎么做的?"

【核心回答框架】

"我采用'分析 → 拆分 → 替换 → 压缩'的四步优化法,每一步都有明确的数据指标。"

背景(Situation): 项目上线后用户反馈加载慢,通过性能分析发现:

  • 包体积过大:生产环境 main.js 5.2MB,首屏加载 4.5s
  • 用户流失:首页跳出率 45%,移动端用户抱怨流量消耗大
  • 业务影响:转化率比竞品低 30%

任务(Task): 将包体积压缩到 500KB 以内,首屏加载降至 2s 以内。

行动(Action)

第一阶段:分析诊断

webpack-bundle-analyzer 体积分析

javascript

javascript
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
      openAnalyzer: true,
      generateStatsFile: true,
      statsFilename: 'bundle-stats.json',
      statsOptions: {
        source: false,
        reasons: false,
        warnings: false,
        errors: false,
        optimizationBailout: false
      },
      excludeAssets: /\.(map|txt)$/,
      logLevel: 'info'
    })
  ]
};

分析报告发现

plain
bundle.js (5.2MB)
├── node_modules (3.8MB - 73%)
│   ├── moment (500KB) ← 包含所有语言包
│   ├── lodash (350KB) ← 全量引入
│   ├── antd (1.2MB) ← 未按需加载
│   ├── echarts (800KB) ← 全量引入
│   └── 其他依赖 (950KB)
├── src (1.2MB - 23%)
│   ├── pages (800KB)
│   └── components (400KB)
└── assets (200KB - 4%)
    ├── images (150KB)
    └── fonts (50KB)

source-map-explorer 源码分析

bash
# 安装
npm install --save-dev source-map-explorer

# 生成 source map
npm run build

# 分析
npx source-map-explorer 'dist/js/*.js'

javascript

javascript
// package.json
{
  "scripts": {
    "analyze": "source-map-explorer 'dist/**/*.js' --html analysis.html"
  }
}

发现问题

plain
main.js 源码占比:
├── moment.js: 500KB (包含 locale/)
├── lodash: 350KB (全量导入)
├── node_modules/antd: 1.2MB
│   ├── es/button: 80KB
│   ├── es/table: 200KB
│   ├── es/form: 150KB
│   └── ... (大量未使用组件)
└── 业务代码: 1.2MB

识别大型依赖库

javascript
// analyze-deps.js
const fs = require('fs');
const path = require('path');

function getDirSize(dirPath) {
  let size = 0;
  const files = fs.readdirSync(dirPath);

  files.forEach(file => {
    const filePath = path.join(dirPath, file);
    const stats = fs.statSync(filePath);

    if (stats.isDirectory()) {
      size += getDirSize(filePath);
    } else {
      size += stats.size;
    }
  });

  return size;
}

const nodeModulesPath = path.join(__dirname, 'node_modules');
const packages = fs.readdirSync(nodeModulesPath);

const packageSizes = packages
  .filter(pkg => !pkg.startsWith('.'))
  .map(pkg => ({
    name: pkg,
    size: getDirSize(path.join(nodeModulesPath, pkg))
  }))
  .sort((a, b) => b.size - a.size);

console.log('📦 Top 20 最大的依赖包:\n');
packageSizes.slice(0, 20).forEach((pkg, i) => {
  console.log(`${i + 1}. ${pkg.name}: ${(pkg.size / 1024 / 1024).toFixed(2)}MB`);
});

分析结果

plain
Top 10 最大依赖:
1. moment: 15.2MB (包含源码 + locale)
2. antd: 12.8MB
3. echarts: 8.5MB
4. lodash: 5.2MB
5. react-dom: 3.8MB
6. @babel/runtime: 2.5MB
7. core-js: 2.1MB
8. axios: 1.8MB
9. react-router-dom: 1.5MB
10. dayjs: 0.5MB

重复依赖检测

bash
# 使用 npm
npm dedupe

# 使用 yarn
yarn dedupe

# 检查重复依赖
npx npm-check-duplicates
javascript
// check-duplicates.js
const fs = require('fs');
const path = require('path');

function findDuplicates(dir, packages = {}, depth = 0) {
  if (depth > 5) return packages; // 防止递归过深

  const nodeModules = path.join(dir, 'node_modules');
  if (!fs.existsSync(nodeModules)) return packages;

  const items = fs.readdirSync(nodeModules);

  items.forEach(item => {
    if (item.startsWith('.')) return;

    const pkgPath = path.join(nodeModules, item, 'package.json');
    if (fs.existsSync(pkgPath)) {
      const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
      const key = pkg.name;
      const version = pkg.version;

      if (!packages[key]) {
        packages[key] = new Set();
      }
      packages[key].add(version);

      // 递归检查嵌套依赖
      findDuplicates(path.join(nodeModules, item), packages, depth + 1);
    }
  });

  return packages;
}

const duplicates = findDuplicates(__dirname);

console.log('🔍 重复依赖检测:\n');
Object.entries(duplicates)
  .filter(([_, versions]) => versions.size > 1)
  .forEach(([name, versions]) => {
    console.log(`⚠️  ${name}: ${Array.from(versions).join(', ')}`);
  });

发现重复

plain
react: 17.0.2, 18.2.0
lodash: 4.17.20, 4.17.21
moment: 2.29.1, 2.29.4

第二阶段:代码分割策略

路由懒加载

React 路由懒加载

javascript
// ❌ 优化前:同步加载
import Dashboard from './pages/Dashboard';
import UserList from './pages/UserList';
import ProductList from './pages/ProductList';
import Settings from './pages/Settings';

const routes = [
  { path: '/dashboard', component: Dashboard },
  { path: '/users', component: UserList },
  { path: '/products', component: ProductList },
  { path: '/settings', component: Settings }
];

// ✅ 优化后:懒加载
import { lazy, Suspense } from 'react';
import Loading from './components/Loading';

const Dashboard = lazy(() => import(/* webpackChunkName: "dashboard" */ './pages/Dashboard'));
const UserList = lazy(() => import(/* webpackChunkName: "users" */ './pages/UserList'));
const ProductList = lazy(() => import(/* webpackChunkName: "products" */ './pages/ProductList'));
const Settings = lazy(() => import(/* webpackChunkName: "settings" */ './pages/Settings'));

const routes = [
  {
    path: '/dashboard',
    component: () => (
      <Suspense fallback={<Loading />}>
        <Dashboard />
      </Suspense>
    )
  },
  // ... 其他路由
];

Vue 路由懒加载

javascript
// ❌ 优化前
import Dashboard from './pages/Dashboard.vue';
import UserList from './pages/UserList.vue';

// ✅ 优化后
const routes = [
  {
    path: '/dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ './pages/Dashboard.vue')
  },
  {
    path: '/users',
    component: () => import(/* webpackChunkName: "users" */ './pages/UserList.vue')
  }
];

// Vue 3 defineAsyncComponent
import { defineAsyncComponent } from 'vue';

const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./pages/Dashboard.vue'),
  loadingComponent: Loading,
  errorComponent: Error,
  delay: 200,
  timeout: 3000
});

预加载关键路由

javascript
// 路由配置
const routes = [
  {
    path: '/dashboard',
    component: lazy(() => import('./pages/Dashboard')),
    preload: true  // 标记为需要预加载
  },
  {
    path: '/users',
    component: lazy(() => import('./pages/UserList'))
  }
];

// 预加载函数
function preloadRoute(route) {
  if (route.preload) {
    // 预加载但不执行
    const Component = route.component;
    if (typeof Component === 'function') {
      Component();
    }
  }
}

// 应用启动时预加载
routes.forEach(preloadRoute);

// 或者鼠标悬停时预加载
<Link
  to="/users"
  onMouseEnter={() => {
    import('./pages/UserList');
  }}
>
  用户管理
</Link>

组件懒加载

javascript
// 动态 import()
const HeavyComponent = lazy(() => import('./components/HeavyChart'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>显示图表</button>

      {showChart && (
        <Suspense fallback={<Skeleton />}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}

// 按需加载第三方组件
const EChartsComponent = lazy(() =>
  import('echarts-for-react').then(module => ({
    default: module.default
  }))
);

// 骨架屏占位
function ChartSkeleton() {
  return (
    <div className="skeleton">
      <div className="skeleton-header" />
      <div className="skeleton-body" />
    </div>
  );
}

SplitChunks 优化配置

javascript
// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,

      cacheGroups: {
        // React 核心库
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
          name: 'react-vendor',
          priority: 30,
          reuseExistingChunk: true
        },

        // UI 组件库
        ui: {
          test: /[\\/]node_modules[\\/](antd|@ant-design)[\\/]/,
          name: 'ui-vendor',
          priority: 20,
          reuseExistingChunk: true
        },

        // 工具库
        utils: {
          test: /[\\/]node_modules[\\/](lodash|moment|dayjs|axios)[\\/]/,
          name: 'utils-vendor',
          priority: 15,
          reuseExistingChunk: true
        },

        // 图表库
        charts: {
          test: /[\\/]node_modules[\\/](echarts|recharts|chart\.js)[\\/]/,
          name: 'charts-vendor',
          priority: 10,
          reuseExistingChunk: true
        },

        // 其他第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          priority: 5,
          reuseExistingChunk: true
        },

        // 公共代码
        common: {
          minChunks: 2,
          priority: 1,
          reuseExistingChunk: true,
          name: 'common'
        }
      }
    },

    // 运行时代码单独提取
    runtimeChunk: {
      name: entrypoint => `runtime-${entrypoint.name}`
    }
  }
};

效果对比

plain
优化前:
├── main.js: 5.2MB (所有代码)

优化后:
├── runtime.js: 2KB
├── react-vendor.js: 150KB
├── ui-vendor.js: 250KB
├── charts-vendor.js: 180KB (按需加载)
├── utils-vendor.js: 80KB
├── common.js: 50KB
├── dashboard.chunk.js: 120KB (首屏)
├── users.chunk.js: 100KB (懒加载)
├── products.chunk.js: 90KB (懒加载)
└── settings.chunk.js: 60KB (懒加载)

首屏加载: 150KB + 250KB + 80KB + 50KB + 120KB = 650KB

第三阶段:依赖优化

替换体积大的库

moment → dayjs
javascript
// ❌ 优化前 (500KB)
import moment from 'moment';
import 'moment/locale/zh-cn';

const date = moment().format('YYYY-MM-DD');

// ✅ 优化后 (7KB)
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import relativeTime from 'dayjs/plugin/relativeTime';

dayjs.extend(relativeTime);
dayjs.locale('zh-cn');

const date = dayjs().format('YYYY-MM-DD');
lodash → lodash-es + tree-shaking
javascript
// ❌ 优化前 (350KB - 全量引入)
import _ from 'lodash';

_.debounce(fn, 300);
_.throttle(fn, 1000);

// ✅ 优化后 (10KB - 按需引入)
import { debounce, throttle } from 'lodash-es';

debounce(fn, 300);
throttle(fn, 1000);

// 或使用原生实现
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
antd 按需加载
javascript
// ❌ 优化前 (1.2MB)
import { Button, Table, Form, Modal } from 'antd';
import 'antd/dist/reset.css';

// ✅ 优化后 - 方案1: babel-plugin-import
// .babelrc
{
  "plugins": [
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": "css"
    }]
  ]
}

// ✅ 优化后 - 方案2: 手动按需引入
import Button from 'antd/es/button';
import Table from 'antd/es/table';
import 'antd/es/button/style/css';
import 'antd/es/table/style/css';
echarts 按需引入
javascript
// ❌ 优化前 (800KB)
import * as echarts from 'echarts';

// ✅ 优化后 (200KB)
import * as echarts from 'echarts/core';
import { BarChart, LineChart, PieChart } from 'echarts/charts';
import {
  GridComponent,
  TooltipComponent,
  LegendComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';

echarts.use([
  BarChart,
  LineChart,
  PieChart,
  GridComponent,
  TooltipComponent,
  LegendComponent,
  CanvasRenderer
]);

体积对比

优化前 优化后 减少
moment → dayjs 500KB 7KB -98.6%
lodash → lodash-es 350KB 10KB -97.1%
antd 按需加载 1.2MB 300KB -75%
echarts 按需引入 800KB 200KB -75%
总计 2.85MB 517KB -81.9%

Tree Shaking 优化

javascript

javascript
// package.json
{
  "sideEffects": [
    "*.css",
    "*.scss",
    "*.less"
  ]
}

// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,  // 标记未使用的导出
    sideEffects: true,  // 删除无副作用的模块
    minimize: true
  }
};

// 确保使用 ESM 格式
// ❌ CommonJS (无法 Tree Shaking)
const utils = require('./utils');

// ✅ ESM (可以 Tree Shaking)
import { add, subtract } from './utils';

验证 Tree Shaking

javascript
// utils.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export function multiply(a, b) {  // 未使用
  return a * b;
}

// main.js
import { add } from './utils';  // 只导入 add

console.log(add(1, 2));

// 构建后 multiply 函数会被删除

externals 外部化

javascript
// webpack.config.js
module.exports = {
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
    'vue': 'Vue',
    'axios': 'axios',
    'lodash': '_',
    'moment': 'moment'
  }
};

// index.html
<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <div id="root"></div>

  <!-- CDN 引入 -->
  <script crossorigin src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
  <script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios@1/dist/axios.min.js"></script>

  <!-- 应用代码 -->
  <script src="/static/js/main.js"></script>
</body>
</html>

第四阶段:代码压缩

JS 压缩

javascript
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true,
            pure_funcs: ['console.log', 'console.info'],
            passes: 2
          },
          format: {
            comments: false
          },
          mangle: {
            safari10: true
          }
        },
        extractComments: false
      })
    ]
  }
};

CSS 压缩

javascript
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin');
const glob = require('glob');
const path = require('path');

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css'
    }),

    // PurgeCSS 移除未使用的 CSS
    new PurgeCSSPlugin({
      paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, {
        nodir: true
      }),
      safelist: {
        standard: [/^ant-/, /^rc-/],  // 保留 antd 样式
        deep: [/^ant-/, /^rc-/],
        greedy: [/data-theme$/]
      }
    })
  ],

  optimization: {
    minimizer: [
      new CssMinimizerPlugin({
        minimizerOptions: {
          preset: [
            'default',
            {
              discardComments: { removeAll: true }
            }
          ]
        }
      })
    ]
  }
};

图片压缩

javascript
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024  // 10KB 以下内联
          }
        },
        use: [
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                progressive: true,
                quality: 75
              },
              optipng: {
                enabled: true
              },
              pngquant: {
                quality: [0.65, 0.90],
                speed: 4
              },
              gifsicle: {
                interlaced: false
              },
              webp: {
                quality: 75
              }
            }
          }
        ]
      }
    ]
  }
};

第五阶段:高级优化

Gzip/Brotli 压缩

javascript
// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [
    // Gzip
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,
      minRatio: 0.8
    }),

    // Brotli (更高压缩率)
    new CompressionPlugin({
      filename: '[path][base].br',
      algorithm: 'brotliCompress',
      test: /\.(js|css|html|svg)$/,
      compressionOptions: {
        level: 11
      },
      threshold: 10240,
      minRatio: 0.8
    })
  ]
};

Nginx 配置

nginx
# gzip 配置
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;

# brotli 配置
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml+xml application/xml+rss text/javascript;

Scope Hoisting

javascript

javascript
// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ],

  optimization: {
    concatenateModules: true
  }
};

结果(Result)
指标 优化前 优化后 提升
总体积 5.2MB 300KB -94.2%
首屏资源 5.2MB 150KB -97.1%
Gzip 后 1.8MB 80KB -95.5%
FCP 2.5s 0.8s -68%
LCP 4.5s 1.2s -73.3%

** **