简历描述模板(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 体积分析
// 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'
})
]
};
分析报告发现:
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 源码分析
# 安装
npm install --save-dev source-map-explorer
# 生成 source map
npm run build
# 分析
npx source-map-explorer 'dist/js/*.js'
// package.json
{
"scripts": {
"analyze": "source-map-explorer 'dist/**/*.js' --html analysis.html"
}
}
发现问题:
main.js 源码占比:
├── moment.js: 500KB (包含 locale/)
├── lodash: 350KB (全量导入)
├── node_modules/antd: 1.2MB
│ ├── es/button: 80KB
│ ├── es/table: 200KB
│ ├── es/form: 150KB
│ └── ... (大量未使用组件)
└── 业务代码: 1.2MB
识别大型依赖库
// 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`);
});
分析结果:
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
重复依赖检测
# 使用 npm
npm dedupe
# 使用 yarn
yarn dedupe
# 检查重复依赖
npx npm-check-duplicates
// 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(', ')}`);
});
发现重复:
react: 17.0.2, 18.2.0
lodash: 4.17.20, 4.17.21
moment: 2.29.1, 2.29.4
第二阶段:代码分割策略
路由懒加载
React 路由懒加载:
// ❌ 优化前:同步加载
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 路由懒加载:
// ❌ 优化前
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
});
预加载关键路由
// 路由配置
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>
组件懒加载
// 动态 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 优化配置
// 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}`
}
}
};
效果对比:
优化前:
├── 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
// ❌ 优化前 (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
// ❌ 优化前 (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 按需加载
// ❌ 优化前 (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 按需引入
// ❌ 优化前 (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 优化
// 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:
// 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 外部化
// 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 压缩
// 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 压缩
// 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 }
}
]
}
})
]
}
};
图片压缩
// 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 压缩
// 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
// 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 采集
// 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 内联
// 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
// 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
}
})
]
};
资源预加载策略
<!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>
动态资源加载
// 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)
// 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
// 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
// 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
// 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
.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
// 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
// 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
// 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
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
// 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
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
// 响应式图片
<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
<!-- 关键 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 路由懒加载 + 预加载
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
最终优化效果对比
打包体积优化
【优化前: 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)
白屏时间优化
【优化前】
├── 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
加载流程对比
优化前:
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
优化后:
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
// Next.js
export async function getServerSideProps() {
// 动态页面使用 SSR
}
export async function getStaticProps() {
return {
props: {},
revalidate: 60 // ISR: 60秒后重新生成
}
}
Q2: 如何平衡首屏体积和用户体验?
回答: 遵循"关键渲染路径"原则:
- 必须同步加载(首屏 < 100KB)
- HTML 结构
- Critical CSS
- 首屏 JS 核心逻辑
- 可以延迟加载
- 非首屏组件
- 图片、视频
- 第三方脚本
- 预加载策略
- 鼠标悬停预加载
- 空闲时预取
- 智能预测下一页
实战案例:
javascript
// 首屏组件立即加载
import Header from './Header';
// 非首屏组件懒加载
const Footer = lazy(() => import('./Footer'));
const Sidebar = lazy(() => import('./Sidebar'));
// 预加载下一页
<Link
to="/products"
onMouseEnter={() => {
import('./pages/Products');
}}
>
产品
</Link>
Q3: 图片优化的完整方案?
回答: 六个层面优化:
- 格式选择
- WebP(体积减少 30%)
- AVIF(更优,兼容性差)
- SVG(矢量图)
- 尺寸适配
- srcset 响应式
- 按设备分辨率
- 压缩优化
- 有损压缩(质量 75-80)
- 无损压缩
- 加载策略
- 懒加载(loading="lazy")
- 渐进式加载
- 占位符(LQIP/BlurHash)
- CDN 加速
- 图片 CDN
- 自动格式转换
- 智能裁剪
- 缓存策略
- 长缓存(1年)
- 版本化 URL
Q4: Service Worker 有什么坑?
回答: 三个常见问题:
- 缓存更新问题
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))
);
})
);
});
- 开发环境干扰
javascript
// 仅生产环境启用
if (process.env.NODE_ENV === 'production') {
navigator.serviceWorker.register('/sw.js');
}
- 跨域问题
javascript
// 设置 CORS 模式
fetch(url, {
mode: 'cors',
credentials: 'include'
});
Q5: Vite 生产构建慢怎么办?
回答: 四个优化方向:
- 减少依赖
javascript
// 替换大型库
moment → dayjs
lodash → lodash-es
- 优化配置
javascript
build: {
rollupOptions: {
output: {
manualChunks: { /* 精细分包 */ }
}
}
}
- 并行压缩
javascript
build: {
minify: 'esbuild', // 比 terser 快 10 倍
terserOptions: {
compress: {
drop_console: true
}
}
}
- 缓存策略
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
// 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'
})
]
};
分析报告发现:
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 源码分析
# 安装
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"
}
}
发现问题:
main.js 源码占比:
├── moment.js: 500KB (包含 locale/)
├── lodash: 350KB (全量导入)
├── node_modules/antd: 1.2MB
│ ├── es/button: 80KB
│ ├── es/table: 200KB
│ ├── es/form: 150KB
│ └── ... (大量未使用组件)
└── 业务代码: 1.2MB
识别大型依赖库
// 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`);
});
分析结果:
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
重复依赖检测
# 使用 npm
npm dedupe
# 使用 yarn
yarn dedupe
# 检查重复依赖
npx npm-check-duplicates
// 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(', ')}`);
});
发现重复:
react: 17.0.2, 18.2.0
lodash: 4.17.20, 4.17.21
moment: 2.29.1, 2.29.4
第二阶段:代码分割策略
路由懒加载
React 路由懒加载:
// ❌ 优化前:同步加载
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 路由懒加载:
// ❌ 优化前
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
});
预加载关键路由
// 路由配置
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>
组件懒加载
// 动态 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 优化配置
// 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}`
}
}
};
效果对比:
优化前:
├── 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
// ❌ 优化前 (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
// ❌ 优化前 (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 按需加载
// ❌ 优化前 (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 按需引入
// ❌ 优化前 (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:
// 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 外部化
// 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 压缩
// 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 压缩
// 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 }
}
]
}
})
]
}
};
图片压缩
// 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 压缩
// 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 配置:
# 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% |
** **