加载性能优化
概述
加载性能直接影响用户体验和业务指标。研究表明,页面加载时间每增加1秒,转化率下降7%。
一、资源优化
1. JavaScript 优化
javascript
// 1. 代码压缩 (Terser)
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除 console
drop_debugger: true, // 移除 debugger
pure_funcs: ['console.log', 'console.info']
},
mangle: true, // 混淆变量名
output: {
comments: false // 移除注释
}
},
parallel: true, // 多线程压缩
extractComments: false
})
]
}
}
// 2. Tree Shaking
// package.json
{
"sideEffects": false // 或指定有副作用的文件
}
// webpack.config.js
module.exports = {
mode: 'production', // 生产模式自动开启 Tree Shaking
optimization: {
usedExports: true
}
}
// 确保使用 ES Modules 导出
// ✅ 可以 Tree Shaking
export function add(a, b) { return a + b }
export function subtract(a, b) { return a - b }
// ❌ 无法 Tree Shaking
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
}
// 3. 按需引入
// ❌ 引入整个库
import _ from 'lodash'
// ✅ 只引入需要的函数
import debounce from 'lodash/debounce'
// ✅ 使用 babel-plugin-import
// babel.config.js
module.exports = {
plugins: [
['import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css'
}]
]
}2. CSS 优化
javascript
// 1. CSS 压缩
// webpack.config.js
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css'
})
],
optimization: {
minimizer: [
new CssMinimizerPlugin()
]
}
}
// 2. 关键 CSS 内联
// 使用 critical 库提取关键 CSS
const critical = require('critical')
critical.generate({
inline: true,
base: 'dist/',
src: 'index.html',
target: 'index-critical.html',
width: 1300,
height: 900
})
// 3. CSS 代码分割
// Vue 单文件组件自动分割
// React 使用 loadable-components
import loadable from '@loadable/component'
const AsyncComponent = loadable(() => import('./Component'), {
fallback: <div>Loading...</div>
})
// 4. 移除无用 CSS
// 使用 PurgeCSS
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const glob = require('glob')
module.exports = {
plugins: [
new PurgeCSSPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
safelist: {
standard: [/^ant-/] // 保留 ant-design 样式
}
})
]
}3. 图片优化
javascript
// 1. 图片压缩
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|jpeg|gif|webp)$/,
use: [
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
quality: 65,
progressive: true
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
webp: {
quality: 75
}
}
}
]
}
]
}
}
// 2. 使用 WebP 格式
<picture>
<source type="image/webp" srcset="image.webp">
<source type="image/jpeg" srcset="image.jpg">
<img src="image.jpg" alt="description">
</picture>
// 3. 响应式图片
<img
srcset="
image-320w.jpg 320w,
image-480w.jpg 480w,
image-800w.jpg 800w
"
sizes="
(max-width: 320px) 280px,
(max-width: 480px) 440px,
800px
"
src="image-800w.jpg"
alt="description"
>
// 4. 小图片 Base64 内联
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8KB 以下转 Base64
}
}
}
]
}
}
// 5. 雪碧图
// 使用 webpack-spritesmith
const SpritesmithPlugin = require('webpack-spritesmith')
module.exports = {
plugins: [
new SpritesmithPlugin({
src: {
cwd: path.resolve(__dirname, 'src/icons'),
glob: '*.png'
},
target: {
image: path.resolve(__dirname, 'src/assets/sprite.png'),
css: path.resolve(__dirname, 'src/assets/sprite.css')
}
})
]
}4. 字体优化
css
/* 1. 字体文件格式优先级 */
@font-face {
font-family: 'MyFont';
src: url('myfont.woff2') format('woff2'), /* 最优 */
url('myfont.woff') format('woff'),
url('myfont.ttf') format('truetype');
font-display: swap; /* 字体加载策略 */
}
/* 2. 字体子集化 */
/* 只包含需要的字符 */
/* 使用 fonttools 或在线工具生成子集 */
@font-face {
font-family: 'MyFont';
src: url('myfont-subset.woff2') format('woff2');
unicode-range: U+4E00-9FFF; /* 中文字符范围 */
}
/* 3. 预加载关键字体 */
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
/* 4. font-display 选项 */
/*
- auto: 浏览器默认行为
- block: 短时间隐藏文本,然后显示后备字体
- swap: 立即显示后备字体,字体加载后替换
- fallback: 短时间不显示,显示后备字体,有限时间内替换
- optional: 只有快速加载才使用自定义字体
*/二、代码分割
1. 路由懒加载
javascript
// Vue Router
const routes = [
{
path: '/home',
component: () => import(/* webpackChunkName: "home" */ './views/Home.vue')
},
{
path: '/about',
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
},
{
path: '/dashboard',
component: () => import(
/* webpackChunkName: "dashboard" */
/* webpackPrefetch: true */
'./views/Dashboard.vue'
)
}
]
// React Router
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
function App() {
return (
<BrowserRouter>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}2. 组件懒加载
vue
<!-- Vue 异步组件 -->
<script setup>
import { defineAsyncComponent } from 'vue'
// 基础用法
const AsyncComp = defineAsyncComponent(() =>
import('./components/AsyncComp.vue')
)
// 高级用法
const AsyncCompWithOptions = defineAsyncComponent({
loader: () => import('./components/HeavyComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200, // 显示 loading 前的延迟
timeout: 10000, // 超时时间
suspensible: false
})
</script>
<template>
<AsyncComp />
<AsyncCompWithOptions />
</template>jsx
// React 懒加载
import React, { lazy, Suspense } from 'react'
// 基础用法
const LazyComponent = lazy(() => import('./LazyComponent'))
// 带命名导出的组件
const LazyNamedComponent = lazy(() =>
import('./Module').then(module => ({
default: module.NamedComponent
}))
)
// 使用
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
)
}
// 使用 @loadable/component (支持 SSR)
import loadable from '@loadable/component'
const LoadableComponent = loadable(() => import('./Component'), {
fallback: <Loading />,
timeout: 10000
})3. Webpack 分包策略
javascript
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000, // 最小 20KB 才分割
minChunks: 1, // 被引用次数
maxAsyncRequests: 30, // 最大并行请求数
maxInitialRequests: 30,
cacheGroups: {
// 第三方库
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
chunks: 'initial'
},
// UI 库单独打包
antd: {
test: /[\\/]node_modules[\\/]antd[\\/]/,
name: 'antd',
priority: 20
},
// 工具库
utils: {
test: /[\\/]node_modules[\\/](lodash|moment|dayjs)[\\/]/,
name: 'utils',
priority: 15
},
// 公共模块
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true
}
}
},
// 运行时代码单独打包
runtimeChunk: {
name: 'runtime'
}
}
}
// Vite 分包
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus'],
'utils': ['lodash-es', 'dayjs']
}
}
}
}
}三、缓存策略
1. HTTP 缓存
javascript
// Nginx 配置
server {
location / {
# HTML 不缓存
if ($request_filename ~* \.html$) {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
# 静态资源强缓存 (带 hash)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API 接口
location /api/ {
add_header Cache-Control "no-cache";
}
}
// Express 设置缓存头
app.use('/static', express.static('public', {
maxAge: '1y', // 强缓存 1 年
immutable: true,
etag: true
}))
app.get('/api/*', (req, res, next) => {
res.set('Cache-Control', 'no-cache')
next()
})2. 文件指纹
javascript
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css'
})
]
}
// 哈希类型:
// - [hash]: 整个项目的 hash,任何文件改变都会变
// - [chunkhash]: 基于 chunk 的 hash,同一 chunk 的文件 hash 相同
// - [contenthash]: 基于文件内容的 hash,推荐使用3. Service Worker 缓存
javascript
// sw.js
const CACHE_NAME = 'v1'
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
]
// 安装
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
.then(() => self.skipWaiting())
)
})
// 激活
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((cacheName) => cacheName !== CACHE_NAME)
.map((cacheName) => 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 (!response || response.status !== 200 || response.type !== 'basic') {
return response
}
// 克隆响应
const responseToCache = response.clone()
// 缓存响应
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache)
})
return response
})
})
)
})
// 使用 Workbox (推荐)
// workbox-config.js
module.exports = {
globDirectory: 'dist/',
globPatterns: ['**/*.{html,js,css,png,jpg,svg}'],
swDest: 'dist/sw.js',
runtimeCaching: [
{
urlPattern: /^https:\/\/api\./,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 300
}
}
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60 // 30 天
}
}
}
]
}四、预加载与预连接
1. 资源预加载
html
<!-- preload: 当前页面需要的关键资源 -->
<link rel="preload" href="critical.js" as="script">
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero.jpg" as="image">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<!-- prefetch: 下一个页面可能需要的资源 -->
<link rel="prefetch" href="next-page.js" as="script">
<!-- prerender: 预渲染整个页面 -->
<link rel="prerender" href="https://example.com/next-page">javascript
// 动态预加载
function preloadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = resolve
img.onerror = reject
img.src = src
})
}
// 预加载下一页资源
function preloadNextPage(path) {
const link = document.createElement('link')
link.rel = 'prefetch'
link.href = path
link.as = 'document'
document.head.appendChild(link)
}
// Vue Router 预加载
router.beforeResolve((to, from, next) => {
// 预加载目标路由的组件
const matched = router.resolve(to).matched
const componentsToPreload = matched
.filter(record => typeof record.components.default === 'function')
.map(record => record.components.default())
Promise.all(componentsToPreload)
next()
})
// Webpack 魔法注释
const Component = () => import(
/* webpackPrefetch: true */
/* webpackPreload: true */
/* webpackChunkName: "my-component" */
'./MyComponent.vue'
)2. DNS 预解析与预连接
html
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//api.example.com">
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="dns-prefetch" href="//analytics.example.com">
<!-- 预连接 (DNS + TCP + TLS) -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>javascript
// 动态预连接
function preconnect(url) {
const link = document.createElement('link')
link.rel = 'preconnect'
link.href = url
link.crossOrigin = 'anonymous'
document.head.appendChild(link)
}
// 在空闲时预连接
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
preconnect('https://api.example.com')
preconnect('https://cdn.example.com')
})
}五、CDN 加速
1. 静态资源 CDN
javascript
// webpack.config.js
module.exports = {
output: {
publicPath: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/assets/'
: '/'
}
}
// vite.config.js
export default {
base: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/'
: '/'
}2. 第三方库 CDN
html
<!-- 使用 CDN 加载第三方库 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4.2.4/dist/vue-router.global.prod.js"></script>javascript
// webpack externals 配置
module.exports = {
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
axios: 'axios',
lodash: '_'
}
}
// html-webpack-plugin 注入 CDN
new HtmlWebpackPlugin({
template: './index.html',
cdn: {
css: [],
js: [
'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js',
'https://cdn.jsdelivr.net/npm/vue-router@4/dist/vue-router.global.prod.js'
]
}
})
// index.html
<!DOCTYPE html>
<html>
<head>
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet">
<% } %>
</head>
<body>
<div id="app"></div>
<% for (var i in htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
</body>
</html>3. CDN 容灾
javascript
// CDN 降级方案
const cdnUrls = {
vue: [
'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js',
'https://unpkg.com/vue@3/dist/vue.global.prod.js',
'https://cdnjs.cloudflare.com/ajax/libs/vue/3.3.4/vue.global.prod.min.js'
]
}
async function loadWithFallback(name) {
const urls = cdnUrls[name]
for (const url of urls) {
try {
await loadScript(url)
console.log(`Loaded ${name} from ${url}`)
return
} catch (error) {
console.warn(`Failed to load ${name} from ${url}`)
}
}
throw new Error(`Failed to load ${name} from all CDN sources`)
}
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
}六、服务端优化
1. Gzip/Brotli 压缩
javascript
// Nginx 配置
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json
application/javascript application/xml+rss
application/atom+xml image/svg+xml;
# Brotli 压缩 (需要 nginx-brotli 模块)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css text/xml application/json
application/javascript application/xml+rss
application/atom+xml image/svg+xml;
// Webpack 预压缩
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
plugins: [
new CompressionPlugin({
filename: '[path][base].gz',
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240, // 10KB 以上才压缩
minRatio: 0.8
}),
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8
})
]
}2. HTTP/2
javascript
// Nginx 配置 HTTP/2
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 服务端推送
location / {
http2_push /css/style.css;
http2_push /js/app.js;
}
}
// HTTP/2 优势:
// 1. 多路复用 - 单个连接多个请求
// 2. 头部压缩 - HPACK 压缩
// 3. 服务端推送 - 主动推送资源
// 4. 二进制传输 - 更高效3. 边缘计算 (Edge)
javascript
// Cloudflare Workers 示例
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const cache = caches.default
let response = await cache.match(request)
if (!response) {
response = await fetch(request)
// 缓存到边缘节点
if (response.ok) {
const responseClone = response.clone()
event.waitUntil(cache.put(request, responseClone))
}
}
return response
}
// Vercel Edge Functions
export const config = {
runtime: 'edge'
}
export default async function handler(request) {
// 在边缘节点执行
return new Response('Hello from Edge!')
}七、首屏优化实战
1. 骨架屏
vue
<!-- SkeletonScreen.vue -->
<template>
<div class="skeleton">
<div class="skeleton-header">
<div class="skeleton-avatar"></div>
<div class="skeleton-title"></div>
</div>
<div class="skeleton-content">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
</div>
</div>
</template>
<style scoped>
.skeleton {
padding: 20px;
}
.skeleton-avatar,
.skeleton-title,
.skeleton-line {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.skeleton-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
}
.skeleton-title {
width: 200px;
height: 20px;
margin-left: 20px;
}
.skeleton-line {
height: 16px;
margin: 10px 0;
}
.skeleton-line.short {
width: 60%;
}
</style>
<!-- 使用 -->
<template>
<SkeletonScreen v-if="loading" />
<div v-else>
<!-- 实际内容 -->
</div>
</template>2. SSR/SSG
javascript
// Nuxt 3 SSR
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true,
nitro: {
prerender: {
routes: ['/about', '/contact']
}
}
})
// Next.js SSR
export async function getServerSideProps(context) {
const data = await fetchData()
return {
props: { data }
}
}
// Next.js SSG
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 60 // ISR: 60秒后重新生成
}
}
// Vite SSR
// entry-server.js
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
import App from './App.vue'
export async function render(url) {
const app = createSSRApp(App)
const html = await renderToString(app)
return html
}3. 性能监控
javascript
// 监控 LCP
new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime)
// 上报
reportMetric('LCP', lastEntry.renderTime || lastEntry.loadTime)
}).observe({ entryTypes: ['largest-contentful-paint'] })
// 监控 FCP
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
console.log('FCP:', entry.startTime)
reportMetric('FCP', entry.startTime)
}
}
}).observe({ entryTypes: ['paint'] })
// 完整的性能监控
class PerformanceMonitor {
constructor() {
this.metrics = {}
this.init()
}
init() {
this.observeLCP()
this.observeFCP()
this.observeFID()
this.observeCLS()
this.observeNavigation()
}
observeLCP() {
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'] })
}
observeFCP() {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
this.metrics.fcp = entry.startTime
}
}
}).observe({ entryTypes: ['paint'] })
}
observeFID() {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.metrics.fid = entry.processingStart - entry.startTime
}
}).observe({ entryTypes: ['first-input'] })
}
observeCLS() {
let clsValue = 0
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value
}
}
this.metrics.cls = clsValue
}).observe({ entryTypes: ['layout-shift'] })
}
observeNavigation() {
window.addEventListener('load', () => {
setTimeout(() => {
const timing = performance.timing
this.metrics.ttfb = timing.responseStart - timing.requestStart
this.metrics.domContentLoaded = timing.domContentLoadedEventEnd - timing.navigationStart
this.metrics.load = timing.loadEventEnd - timing.navigationStart
}, 0)
})
}
report() {
console.log('Performance Metrics:', this.metrics)
// 上报到监控系统
}
}
const monitor = new PerformanceMonitor()
window.addEventListener('beforeunload', () => {
monitor.report()
})常见面试题
1. 首屏加载慢如何优化?
点击查看答案
优化策略:
资源优化
- 代码压缩 (JS/CSS/HTML)
- Tree Shaking
- 图片优化 (WebP、压缩、懒加载)
加载优化
- 路由懒加载
- 组件懒加载
- 代码分割 (splitChunks)
- 预加载关键资源 (preload)
缓存优化
- HTTP 缓存策略
- Service Worker
- 文件指纹 (contenthash)
网络优化
- CDN 加速
- Gzip/Brotli 压缩
- HTTP/2
- DNS 预解析
渲染优化
- SSR/SSG
- 骨架屏
- 关键 CSS 内联
监控分析
- Lighthouse 审计
- 性能监控
- 用户体验指标
2. 如何实现按需加载?
点击查看答案
javascript
// 1. 路由懒加载
const routes = [
{
path: '/home',
component: () => import('./views/Home.vue')
}
]
// 2. 组件懒加载
const AsyncComponent = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
// 3. 第三方库按需引入
import debounce from 'lodash/debounce' // 而非 import _ from 'lodash'
// 4. 条件加载
if (needFeature) {
const module = await import('./feature')
module.init()
}
// 5. Webpack 魔法注释
const Component = () => import(
/* webpackChunkName: "my-chunk" */
/* webpackPrefetch: true */
'./Component.vue'
)总结
加载优化清单
资源层面:
- [ ] JavaScript 压缩和 Tree Shaking
- [ ] CSS 压缩和无用代码移除
- [ ] 图片压缩和格式优化 (WebP)
- [ ] 字体子集化
加载层面:
- [ ] 代码分割和懒加载
- [ ] 预加载关键资源
- [ ] DNS 预解析
- [ ] CDN 加速
缓存层面:
- [ ] HTTP 强缓存和协商缓存
- [ ] Service Worker
- [ ] 文件指纹
服务器层面:
- [ ] Gzip/Brotli 压缩
- [ ] HTTP/2
- [ ] SSR/SSG