前端首屏渲染优化:完整实战指南
首屏加载性能直接影响用户的第一印象、留存率和转化率。根据数据,页面加载超过3秒会导致超过40%的用户流失。本文提供全面、实用的首屏优化技术方案,从性能指标到实战案例,帮助你构建高效流畅的用户体验。
一、性能关键指标
优化首屏渲染前,需先了解核心性能指标:
指标 | 描述 | 理想值 |
---|---|---|
FCP (First Contentful Paint) | 首次显示内容时间,即”白屏时间” | <1.8秒 |
Speed Index | 可视内容显示速度,衡量”首屏时间” | <3.4秒 |
TTI (Time To Interactive) | 页面可交互时间 | <3.8秒 |
LCP (Largest Contentful Paint) | 最大内容渲染时间 | <2.5秒 |
Lighthouse Performance Score | Chrome浏览器性能评分 | >90分 |
性能调试工具
- Chrome DevTools Network面板:分析资源加载瀑布流
- Lighthouse:全面评估并提供优化建议
- WebPageTest:多区域、多设备性能测试
- K6:负载测试工具
- Hiper:命令行性能分析工具
- PerformanceTiming API:代码中监控性能指标
二、资源优化策略
1. 资源压缩与拆分
代码拆分优化:
// webpack.config.js - 优化配置示例
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除console
drop_debugger: true // 移除debugger
},
output: {
comments: false // 移除注释
}
},
parallel: true // 并行压缩
}),
new CssMinimizerPlugin({
minimizerOptions: {
preset: ['default', { discardComments: { removeAll: true } }]
}
})
],
splitChunks: {
chunks: 'all',
minSize: 20000, // 最小尺寸
maxSize: 250000, // 最大尺寸
minChunks: 1, // 最小引用次数
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors'
},
common: {
name: 'common',
minChunks: 2,
priority: -20,
reuseExistingChunk: true
},
// 分离大型依赖库
echarts: {
test: /[\\/]node_modules[\\/]echarts/,
name: 'echarts',
priority: 20
},
lodash: {
test: /[\\/]node_modules[\\/]lodash/,
name: 'lodash',
priority: 20
}
}
}
}
}
动态导入优化:
// 使用动态import进行代码分割
const FooComponent = () => import(/* webpackChunkName: "foo-component" */ './FooComponent.vue');
// 在路由中使用
const routes = [
{
path: '/foo',
component: FooComponent,
// 预获取下一级页面
beforeEnter: (to, from, next) => {
// 预取可能的下一个页面
import(/* webpackPrefetch: true, webpackChunkName: "bar-component" */ './BarComponent.vue');
next();
}
}
];
Gzip压缩配置:
# Nginx开启gzip压缩配置
http {
gzip on;
gzip_comp_level 6;
gzip_min_length 1k;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype;
gzip_vary on;
gzip_proxied any;
gzip_disable "MSIE [1-6]\.";
}
图片优化:
<!-- 响应式图片优化示例 -->
<picture>
<!-- WebP格式优先 -->
<source type="image/webp" srcset="./banner-small.webp 400w, ./banner-medium.webp 800w, ./banner-large.webp 1200w">
<!-- 降级处理 -->
<source type="image/jpeg" srcset="./banner-small.jpg 400w, ./banner-medium.jpg 800w, ./banner-large.jpg 1200w">
<!-- 默认图片 -->
<img
src="./banner-small.jpg"
srcset="./banner-small.jpg 400w, ./banner-medium.jpg 800w, ./banner-large.jpg 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
loading="lazy"
alt="Banner image"
width="800"
height="450">
</picture>
图片延迟加载:
// 使用VueLazyload插件
import Vue from 'vue';
import VueLazyload from 'vue-lazyload';
Vue.use(VueLazyload, {
preLoad: 1.3,
error: './error.png',
loading: './loading.gif',
attempt: 1,
// 监听滚动容器
listenEvents: ['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend']
});
// 在模板中使用
// <img v-lazy="image.src" alt="图片">
JavaScript优化:
- Tree Shaking移除无用代码
- 代码分割与按需加载
- 延迟加载非关键资源
// 按需引入第三方库
import { map, filter } from 'lodash-es';
// 而不是
// import _ from 'lodash';
CSS优化:
- 关键CSS内联(减少阻塞渲染的CSS)
- 移除未使用的CSS规则
// 使用PurgeCSS移除未使用CSS
// postcss.config.js
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.vue', './src/**/*.js', './public/**/*.html'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
safelist: [/^el-/, /^ant-/, /^router-/, /^swiper-/] // 保留组件库类名
})
]
}
2. HTTP协议优化
HTTP/2多路复用:
HTTP/1.1的keep-alive模式下,请求会按顺序阻塞执行,而HTTP/2支持多路复用,可以并行发送多个请求,显著提升加载性能。
# 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;
# 其他配置...
}
3. 缓存策略优化
强缓存与协商缓存结合使用:
# Nginx缓存配置最佳实践
location ~* \.(js|css)$ {
add_header Cache-Control "max-age=31536000, immutable";
etag on;
}
location ~* \.(html)$ {
add_header Cache-Control "no-cache";
etag on;
}
location ~* \.(png|jpg|jpeg|gif|webp|svg|ico)$ {
add_header Cache-Control "max-age=31536000, immutable";
etag on;
}
最佳缓存策略:
- JS/CSS文件:使用内容哈希文件名+长期缓存
- HTML:使用协商缓存,确保内容更新
- 图片/字体:强缓存+内容哈希文件名
4. 减少HTTP请求
合并资源请求:
// webpack合并配置
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 5, // 限制生成的chunk数量
minChunkSize: 10000 // 最小chunk大小
})
CSS Sprites与图标字体:
/* 使用图标字体替代小图标 */
@font-face {
font-family: 'MyIcons';
src: url('myicons.woff2') format('woff2');
font-display: block;
}
.icon-home:before {
font-family: 'MyIcons';
content: '\e901';
}
Data URL内联小资源:
/* 将小图片转换为Data URL */
.small-icon {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53...');
}
三、首屏白屏优化
1. 骨架屏实现
骨架屏为用户提供加载中的视觉反馈,适用于数据密集型页面。
<!-- 骨架屏组件示例 (SkeletonCard.vue) -->
<template>
<div class="skeleton-card">
<div class="skeleton-img"></div>
<div class="skeleton-content">
<div class="skeleton-title"></div>
<div class="skeleton-text"></div>
<div class="skeleton-text"></div>
</div>
</div>
</template>
<style scoped>
.skeleton-card {
padding: 16px;
border-radius: 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.skeleton-img {
height: 120px;
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%);
background-size: 400% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
margin-bottom: 16px;
}
.skeleton-title {
height: 24px;
width: 80%;
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%);
background-size: 400% 100%;
animation: shimmer 1.5s infinite;
margin-bottom: 12px;
border-radius: 2px;
}
.skeleton-text {
height: 16px;
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%);
background-size: 400% 100%;
animation: shimmer 1.5s infinite;
margin-bottom: 8px;
border-radius: 2px;
}
@keyframes shimmer {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
</style>
2. 路由懒加载优化
// Vue Router懒加载实现
const routes = [
{
path: '/',
component: () => import(/* webpackChunkName: "home" */ './views/Home.vue'),
// 预加载下一个可能访问的页面
beforeEnter: (to, from, next) => {
// 预取可能的下一个页面
import(/* webpackPrefetch: true, webpackChunkName: "about" */ './views/About.vue');
next();
}
},
{
path: '/about',
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
},
{
path: '/dashboard',
component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue')
}
]
优化组件异步加载体验:
// 带加载指示器的异步组件
const AsyncComponent = () => ({
component: import('./ComplicatedComponent.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 200, // 展示加载组件前的延迟时间
timeout: 3000 // 最长等待时间
})
3. 预渲染实现
适用于内容较为静态的页面,可显著提升首屏渲染速度和SEO效果。
// vue.config.js
const path = require('path');
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
module.exports = {
configureWebpack: {
plugins: [
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, 'dist'),
routes: ['/', '/about', '/contact'],
renderer: new Renderer({
headless: true,
renderAfterDocumentEvent: 'render-event',
renderAfterTime: 5000,
captureConsole: true,
maxConcurrentRoutes: 4, // 并发预渲染路由数量
inject: {
foo: 'bar'
}
}),
postProcess(renderedRoute) {
renderedRoute.route = renderedRoute.originalRoute;
// 压缩HTML
renderedRoute.html = renderedRoute.html
.replace(/\s{2,}/g, ' ')
.replace(/\n/g, '')
.trim();
return renderedRoute;
}
})
]
}
}
// main.js
new Vue({
router,
store,
render: h => h(App),
mounted() {
// 触发渲染事件
document.dispatchEvent(new Event('render-event'));
}
}).$mount('#app');
4. SSR (服务端渲染)
适用场景:
- 内容SEO要求高
- 首屏加载速度要求极高
- 动态内容丰富的大型应用
Nuxt.js SSR优化配置:
// nuxt.config.js
export default {
render: {
// 减少HTTP请求
resourceHints: false,
// HTTP/2服务器推送
http2: {
push: true,
pushAssets: (req, res, publicPath, preloadFiles) =>
preloadFiles
.filter(f => f.asType === 'script' && f.file === 'runtime.js')
.map(f => `<${publicPath}${f.file}>; rel=preload; as=${f.asType}`)
},
// 压缩设置
compressor: {
level: 6
},
// 静态资源缓存
static: {
maxAge: '1y'
}
},
build: {
// 文件名格式,用于缓存
filenames: {
app: ({ isDev }) => isDev ? '[name].js' : '[name].[contenthash:8].js',
chunk: ({ isDev }) => isDev ? '[name].js' : '[name].[contenthash:8].js',
css: ({ isDev }) => isDev ? '[name].css' : '[name].[contenthash:8].css',
img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[name].[contenthash:8].[ext]',
font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[name].[contenthash:8].[ext]',
video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[name].[contenthash:8].[ext]'
},
// 拆分模块
splitChunks: {
layouts: true,
pages: true,
commons: true
},
// 优化设置
optimization: {
minimize: true,
splitChunks: {
chunks: 'all',
automaticNameDelimiter: '.',
name: undefined,
minSize: 20000,
maxSize: 250000
}
},
// transpile certain dependencies
transpile: ['lodash-es', 'vue-lazy-hydration']
},
// 性能优化
performance: {
gzip: true
}
}
5. Service Workers与PWA
利用Service Workers实现离线加载,提升重复访问性能。
// 注册Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(registration => {
console.log('ServiceWorker registration successful with scope:', registration.scope);
}).catch(error => {
console.log('ServiceWorker registration failed: ', error);
});
});
}
// sw.js - Service Worker缓存策略
const CACHE_NAME = 'my-site-cache-v1';
const STATIC_CACHE = 'static-cache-v1';
const DYNAMIC_CACHE = 'dynamic-cache-v1';
// 预缓存的静态资源
const staticAssets = [
'/',
'/index.html',
'/css/main.css',
'/js/main.js',
'/images/logo.png',
'/offline.html' // 离线页面
];
// 安装事件
self.addEventListener('install', event => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => {
console.log('Opened cache');
return cache.addAll(staticAssets);
})
.then(() => self.skipWaiting()) // 立即激活新SW
);
});
// 激活事件 - 清理旧缓存
self.addEventListener('activate', event => {
const cacheWhitelist = [STATIC_CACHE, DYNAMIC_CACHE];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName); // 删除旧缓存
}
})
);
}).then(() => self.clients.claim()) // 控制所有客户端
);
});
// 网络优先,缓存回退策略
self.addEventListener('fetch', event => {
// 跳过非GET请求和浏览器扩展请求
if (event.request.method !== 'GET' || event.request.url.startsWith('chrome-extension')) {
return;
}
event.respondWith(
fetch(event.request)
.then(response => {
// 检查响应有效性
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应
const responseToCache = response.clone();
// 动态缓存
caches.open(DYNAMIC_CACHE)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// 网络请求失败时回退到缓存
return caches.match(event.request)
.then(cachedResponse => {
return cachedResponse || caches.match('/offline.html');
});
})
);
});
四、资源加载优化
1. Preload和Prefetch技术
Preload:用于加载当前页面重要资源,高优先级。 Prefetch:用于加载未来可能需要的资源,低优先级。
<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/roboto.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">
<link rel="preload" href="/js/main.js" as="script">
<!-- 预获取未来可能需要的资源 -->
<link rel="prefetch" href="/js/non-critical.js">
<link rel="prefetch" href="/css/non-critical.css">
自动注入预加载链接的Webpack配置:
// vue.config.js
module.exports = {
chainWebpack: config => {
// 自动注入preload和prefetch
config.plugin('preload').tap(options => {
options[0].include = 'initial';
// 不预加载图片或视频等二进制资源
options[0].fileBlacklist = [/\.map$/, /hot-update\.js$/, /\.(png|jpe?g|gif|webp)$/];
return options;
});
config.plugin('prefetch').tap(options => {
options[0].include = 'asyncChunks';
// 预获取限制
options[0].fileBlacklist = [/\.map$/, /hot-update\.js$/];
return options;
});
}
}
2. 字体优化
/* 字体子集化和分阶段加载 */
@font-face {
font-family: 'CustomFont';
src: url('customfont-subset.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap; /* 先显示系统字体,字体加载完成后再替换 */
unicode-range: U+4E00-9FFF; /* 仅加载中文字符 */
}
/* 针对不同字重使用不同子集 */
@font-face {
font-family: 'CustomFont';
src: url('customfont-bold-latin.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
3. 图片优化最佳实践
<!-- 最佳图片优化实践 -->
<picture>
<!-- WebP格式优先 -->
<source type="image/webp" srcset="image-small.webp 400w, image-medium.webp 800w, image-large.webp 1200w">
<!-- AVIF格式支持 -->
<source type="image/avif" srcset="image-small.avif 400w, image-medium.avif 800w, image-large.avif 1200w">
<!-- 降级处理 -->
<source type="image/jpeg" srcset="image-small.jpg 400w, image-medium.jpg 800w, image-large.jpg 1200w">
<!-- 默认图片 -->
<img
src="image-small.jpg"
alt="描述性替代文本"
width="800"
height="600"
loading="lazy"
decoding="async">
</picture>
4. 使用CDN加速
// vue.config.js - 配置外部CDN资源
module.exports = {
configureWebpack: {
externals: process.env.NODE_ENV === 'production' ? {
'vue': 'Vue',
'vuex': 'Vuex',
'vue-router': 'VueRouter',
'axios': 'axios',
'echarts': 'echarts'
} : {},
},
chainWebpack: config => {
// HTML模板注入CDN
config.plugin('html').tap(args => {
args[0].cdn = process.env.NODE_ENV === 'production' ? {
css: [
'https://cdn.jsdelivr.net/npm/element-ui@2.15.6/lib/theme-chalk/index.css'
],
js: [
'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js',
'https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js',
'https://cdn.jsdelivr.net/npm/vue-router@3.5.3/dist/vue-router.min.js',
'https://cdn.jsdelivr.net/npm/axios@0.24.0/dist/axios.min.js',
'https://cdn.jsdelivr.net/npm/echarts@5.3.0/dist/echarts.min.js'
]
} : {};
return args;
});
}
}
5. Script加载优化
<!-- 合理使用async和defer -->
<script defer src="/js/vendor.js"></script>
<script async src="/js/analytics.js"></script>
<!-- 模块化脚本 -->
<script type="module" src="/js/app.mjs"></script>
<script nomodule src="/js/app-legacy.js"></script>
// 动态加载非关键脚本
function loadScript(src, callback) {
const script = document.createElement('script');
script.src = src;
script.async = true;
script.onload = callback;
script.onerror = () => {
console.error('Script load error: ' + src);
};
document.head.appendChild(script);
}
// 在适当时机加载
window.addEventListener('load', () => {
// 页面加载完成后再加载非关键脚本
setTimeout(() => {
loadScript('/js/non-critical.js', () => {
console.log('非关键脚本加载完成');
});
}, 2000);
});
五、渲染优化技术
1. 关键渲染路径优化
将关键CSS内联到HTML中,减少渲染阻塞:
<head>
<!-- 内联关键CSS -->
<style>
/* 首屏关键样式 */
.header { padding: 1rem; background: #f8f9fa; }
.hero { height: 60vh; background: #007bff; color: white; }
.main-nav { display: flex; justify-content: space-between; }
/* 仅包含首屏所需的最小CSS */
</style>
<!-- 异步加载完整CSS -->
<link rel="preload" href="/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>
</head>
2. 避免回流和重绘
// 批量DOM操作
function updateElements() {
// 不好的做法 - 多次触发回流
/*
for (let i = 0; i < 100; i++) {
document.getElementById('container').innerHTML += '<div>' + i + '</div>';
}
*/
// 好的做法 - 只触发一次回流
const container = document.getElementById('container');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = i;
fragment.appendChild(div);
}
container.appendChild(fragment);
}
// 使用CSS类一次性应用多个样式变更
function optimizedAnimation() {
const element = document.getElementById('animated');
// 不好的做法
/*
element.style.width = '100px';
element.style.height = '100px';
element.style.backgroundColor = 'red';
*/
// 好的做法
element.classList.add('animated-state');
}
3. 移动端性能优化
/* 移动端优先的CSS写法 */
/* 基础样式针对移动设备 */
.container {
padding: 10px;
font-size: 14px;
}
/* 媒体查询用于更大屏幕 */
@media (min-width: 768px) {
.container {
padding: 20px;
font-size: 16px;
}
}
/* 硬件加速 */
.hardware-accelerated {
transform: translateZ(0);
will-change: transform; /* 慎用,只在真正需要的元素上使用 */
}
/* 优化滚动性能 /
.scroll-container {
overflow-y: scroll;
-webkit-overflow-scrolling: touch; / 流畅滚动 */
height: 100vh;
}
/* 减少渲染层 /
.layer {
transform: translate3d(0, 0, 0); / 创建新的渲染层 */
z-index: 1;
}
六、框架特定优化
1. Vue.js性能优化
// 1. 使用v-show替代v-if (对频繁切换的内容)
<div v-show="isVisible">频繁切换的内容</div>
// 2. 使用函数式组件
Vue.component('functional-component', {
functional: true,
render(h, { props }) {
return h('div', props.text);
}
});
// 3. 使用keep-alive缓存组件
<keep-alive :include="['ComponentA', 'ComponentB']">
<router-view/>
</keep-alive>
// 4. 大列表性能优化
<script>
import VirtualList from 'vue-virtual-scroll-list';
export default {
components: {
VirtualList
},
data() {
return {
items: Array(10000).fill().map((_, i) => ({ id: i, text: `Item ${i}` }))
};
}
}
</script>
<template>
<virtual-list
:data-key="'id'"
:data-sources="items"
:data-component="itemComponent"
:estimate-size="50"
/>
</template>
// 5. 使用非响应式数据存储大量数据
export default {
data() {
return {
reactiveData: {}
}
},
created() {
// 非响应式数据,不触发Vue的响应式系统
this.nonReactiveData = {
largeArray: new Array(100000)
};
}
}
2. React性能优化
// 1. 使用React.memo减少不必要的渲染
const MemoizedComponent = React.memo(function MyComponent(props) {
// 只有当props变化时才会重新渲染
return (
<div>{props.name}</div>
);
});
// 2. 使用useMemo缓存计算结果
function ExpensiveComponent({ data }) {
// 只有当data变化时才会重新计算
const processedData = React.useMemo(() => {
return expensiveCalculation(data);
}, [data]);
return <div>{processedData}</div>;
}
// 3. 使用useCallback缓存函数引用
function Parent() {
const [count, setCount] = React.useState(0);
// 只有当count变化时才会创建新函数
const handleClick = React.useCallback(() => {
console.log('Clicked', count);
}, [count]);
return <Child onClick={handleClick} />;
}
// 4. 列表优化 - 使用唯一key
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}
// 5. 代码分割与懒加载
import React, { Suspense, lazy } from 'react';
// 懒加载组件
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
七、极致优化实战案例
1. 核心库分离与优化
// vue.config.js
module.exports = {
configureWebpack: {
externals: process.env.NODE_ENV === 'production' ? {
'vue': 'Vue',
'vuex': 'Vuex',
'vue-router': 'VueRouter',
'axios': 'axios',
'echarts': 'echarts'
} : {},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 20000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// 获取依赖包名称
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
// 避免文件名过长导致的哈希冲突
return `npm.${packageName.replace('@', '')}`;
}
}
}
}
}
},
chainWebpack: config => {
// index.html模板注入
config.plugin('html').tap(args => {
args[0].cdn = process.env.NODE_ENV === 'production' ? {
css: [
'https://cdn.jsdelivr.net/npm/element-ui@2.15.6/lib/theme-chalk/index.css'
],
js: [
'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js',
'https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js',
'https://cdn.jsdelivr.net/npm/vue-router@3.5.3/dist/vue-router.min.js',
'https://cdn.jsdelivr.net/npm/axios@0.24.0/dist/axios.min.js',
'https://cdn.jsdelivr.net/npm/echarts@5.3.0/dist/echarts.min.js'
]
} : {};
return args;
});
}
}
htmlCopy<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>优化示例</title>
<!-- 预连接DNS -->
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<!-- 关键CSS内联 -->
<style>
/* 首屏关键样式 */
.app-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
background: #f5f5f5;
z-index: 999;
}
.app-loading-spinner {
width: 50px;
height: 50px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<!-- CDN资源注入 -->
<% for (var css of htmlWebpackPlugin.options.cdn?.css || []) { %>
<link rel="stylesheet" href="<%= css %>" />
<% } %>
</head>
<body>
<div id="app">
<!-- 初始加载状态 -->
<div class="app-loading">
<div class="app-loading-spinner"></div>
</div>
</div>
<!-- CDN JS资源注入 -->
<% for (var js of htmlWebpackPlugin.options.cdn?.js || []) { %>
<script src="<%= js %>"></script>
<% } %>
<!-- 异步加载统计脚本 -->
<script>
window.addEventListener('load', function() {
setTimeout(function() {
var script = document.createElement('script');
script.src = 'https://www.google-analytics.com/analytics.js';
script.async = true;
document.head.appendChild(script);
}, 3000); // 延迟3秒加载分析脚本
});
</script>
</body>
</html>
2. 优化前后对比
优化手段优化前优化后提升
核心代码拆分1.2MB主包350KB主包减少70%
图片优化5秒加载0.5秒加载提速90%
CDN加速本地加载CDN分发TTFB减少200ms
关键CSS内联FCP 1.8sFCP 0.5s提速72%
SSR实现TTI 4.5sTTI 1.2s提速73%
骨架屏白屏1.5s视觉反馈<0.1s体验提升93%
Gzip压缩1MB传输300KB传输减少70%
懒加载组件4MB初始加载1.1MB初始加载减少72%
3. 性能监控实现
// performance-monitor.js
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.init();
}
init() {
// 页面加载性能
this.captureNavigationTiming();
// 首次内容绘制
this.capturePaintTiming();
// 资源加载性能
this.captureResourceTiming();
// 用户交互性能
this.captureUserInteraction();
// 发送性能数据
window.addEventListener('beforeunload', () => {
this.sendMetrics();
});
}
captureNavigationTiming() {
window.addEventListener('load', () => {
setTimeout(() => {
const performance = window.performance;
if (performance) {
const navEntry = performance.getEntriesByType('navigation')[0];
const timing = navEntry || performance.timing;
this.metrics.navigation = {
// DNS查询时间
dnsLookup: timing.domainLookupEnd - timing.domainLookupStart,
// TCP连接时间
tcpConnection: timing.connectEnd - timing.connectStart,
// 服务器响应时间
serverResponse: timing.responseStart - timing.requestStart,
// DOM解析时间
domParse: timing.domInteractive - timing.responseEnd,
// 资源加载时间
resourceLoad: timing.loadEventStart - timing.domContentLoadedEventEnd,
// 总页面加载时间
pageLoad: timing.loadEventStart - timing.navigationStart,
// DOM Ready时间
domReady: timing.domContentLoadedEventEnd - timing.navigationStart
};
}
}, 0);
});
}
capturePaintTiming() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.metrics[entry.name] = entry.startTime;
console.log(`${entry.name}: ${entry.startTime}ms`);
}
});
observer.observe({ entryTypes: ['paint', 'largest-contentful-paint'] });
}
captureResourceTiming() {
const resourceObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
// 按资源类型分组
const resources = entries.reduce((acc, entry) => {
const type = entry.initiatorType;
if (!acc[type]) acc[type] = [];
acc[type].push({
name: entry.name,
duration: entry.duration,
size: entry.transferSize,
startTime: entry.startTime
});
return acc;
}, {});
this.metrics.resources = resources;
});
resourceObserver.observe({ entryTypes: ['resource'] });
}
captureUserInteraction() {
// 测量交互到视觉反馈的延迟
const interactionObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`Interaction: ${entry.duration}ms`);
if (!this.metrics.interactions) this.metrics.interactions = [];
this.metrics.interactions.push({
type: entry.name,
startTime: entry.startTime,
duration: entry.duration
});
}
});
interactionObserver.observe({ entryTypes: ['first-input', 'event'] });
}
sendMetrics() {
// 实际项目中发送到分析服务器
console.log('Performance metrics:', this.metrics);
// 使用Beacon API异步发送,不阻塞页面卸载
if (navigator.sendBeacon) {
navigator.sendBeacon('/analytics/performance', JSON.stringify(this.metrics));
} else {
// 降级方案
fetch('/analytics/performance', {
method: 'POST',
body: JSON.stringify(this.metrics),
keepalive: true
});
}
}
}
// 初始化监控
new PerformanceMonitor();
八、避坑指南与最佳实践
1. 常见陷阱与解决方案
CSS阻塞渲染问题
<!-- 不好的方式:直接加载CSS -->
<link rel="stylesheet" href="/css/main.css">
<!-- 好的方式:关键CSS内联 + 异步加载非关键CSS -->
<style>
/* 关键CSS内联 */
header, .hero { /* 首屏样式 */ }
</style>
<link rel="preload" href="/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>
字体闪烁(FOUT)问题
/* 不好的方式 */
@font-face {
font-family: 'CustomFont';
src: url('customfont.woff2') format('woff2');
}
/* 好的方式 */
@font-face {
font-family: 'CustomFont';
src: url('customfont.woff2') format('woff2');
font-display: swap; /* 系统字体先显示,加载完后替换 */
}
/* 更好的方式:预加载 + swap */
/*
<link rel="preload" href="customfont.woff2" as="font" type="font/woff2" crossorigin>
*/
过度优化陷阱
// 不好的实践:盲目合并所有文件
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1 // 将所有文件合并成一个包
})
// 好的实践:按变更频率拆分
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
// 框架代码(变化少)
framework: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'framework',
chunks: 'all',
},
// UI库(中等变化)
ui: {
test: /[\\/]node_modules[\\/](antd|@material-ui)[\\/]/,
name: 'ui',
chunks: 'all',
},
// 业务逻辑(变化频繁)
business: {
test: /[\\/]src[\\/]pages[\\/]/,
name: 'business',
chunks: 'all',
}
}
}
}
}
监控脚本阻塞问题
<!-- 不好的方式 -->
<script src="https://www.google-analytics.com/analytics.js"></script>
<!-- 好的方式:异步加载 -->
<script>
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXX');
</script>
2. 移动端性能最佳实践
// 1. 避免内存泄漏
function setupEventListener() {
const button = document.getElementById('button');
let counter = 0;
// 不好的方式 - 可能导致内存泄漏
/*
button.addEventListener('click', function() {
counter++;
console.log('Counter:', counter);
});
*/
// 好的方式 - 保存引用以便移除
const clickHandler = function() {
counter++;
console.log('Counter:', counter);
};
button.addEventListener('click', clickHandler);
// 组件卸载时移除事件监听
return function cleanup() {
button.removeEventListener('click', clickHandler);
};
}
// 2. 滚动性能优化
const scrollContainer = document.querySelector('.scroll-container');
// 触发硬件加速
scrollContainer.style.transform = 'translateZ(0)';
// 使用节流控制滚动事件处理频率
function throttle(fn, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return fn(...args);
};
}
// 节流处理滚动事件
scrollContainer.addEventListener('scroll', throttle(function() {
// 执行滚动处理逻辑
console.log('Scroll processed');
}, 100));
3. 从核心Web指标角度优化
// 1. LCP (Largest Contentful Paint) 优化
// 预加载大型hero图片
const heroImage = document.querySelector('.hero-image');
if (heroImage) {
const imgUrl = heroImage.dataset.src;
const preloadLink = document.createElement('link');
preloadLink.rel = 'preload';
preloadLink.as = 'image';
preloadLink.href = imgUrl;
document.head.appendChild(preloadLink);
// 图片加载后移除预加载标签
heroImage.onload = () => {
preloadLink.remove();
};
// 设置src以加载图片
heroImage.src = imgUrl;
}
// 2. FID (First Input Delay) 优化
// 将长任务拆分
function longTask(inputData) {
// 如果任务预计耗时超过50ms,考虑拆分或异步处理
if (inputData.length > 1000) {
// 将任务拆分为小块
requestIdleCallback(() => {
const chunk = inputData.slice(0, 1000);
processChunk(chunk);
// 处理剩余部分
setTimeout(() => {
longTask(inputData.slice(1000));
}, 0);
});
} else {
// 直接处理小任务
processChunk(inputData);
}
}
// 3. CLS (Cumulative Layout Shift) 优化
// 预设图片尺寸避免布局偏移
document.querySelectorAll('img').forEach(img => {
if (!img.hasAttribute('width') || !img.hasAttribute('height')) {
// 设置预留空间,防止加载时布局偏移
img.style.aspectRatio = '16/9';
}
});
// 对于动态加载的内容,预留空间
const dynamicContentContainer = document.querySelector('.dynamic-content');
if (dynamicContentContainer) {
dynamicContentContainer.style.minHeight = '500px'; // 预留空间
}
九、新兴技术与未来趋势
1. HTTP/3 与 QUIC
# NGINX配置HTTP/3 (实验性)
# 需要特定版本的NGINX和其他依赖
server {
listen 443 quic reuseport;
listen 443 ssl http2;
http3 on;
http3_max_concurrent_streams 256;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_early_data on;
# Alt-Svc header告知浏览器支持HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400';
}
2. ESM (ES Modules) 优化
htmlCopy<!-- 使用原生ESM -->
<script type="module">
// 导入优化
import { reactive, computed } from 'https://cdn.jsdelivr.net/npm/vue@3.2.31/dist/vue.esm-browser.js';
const state = reactive({
count: 0
});
const doubleCount = computed(() => state.count * 2);
// 模块代码
</script>
3. 边缘计算 (Edge Computing)
// Cloudflare Workers示例 - 动态缓存和图片处理
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
// 解析URL获取请求信息
const url = new URL(request.url);
// 图片处理逻辑
if (url.pathname.startsWith('/images/')) {
const imageParams = new URLSearchParams(url.search);
const width = imageParams.get('width');
const height = imageParams.get('height');
const format = imageParams.get('format') || 'webp';
// 构建缓存键
const cacheKey = new Request(url.toString(), request);
const cache = caches.default;
// 检查缓存
let response = await cache.match(cacheKey);
if (response) {
return response;
}
// 获取原始图片
const imageUrl = url.pathname.replace('/images/', '/original-images/');
const originalResponse = await fetch(new URL(imageUrl, url.origin));
if (!originalResponse.ok) {
return originalResponse;
}
// 处理图片 (使用Cloudflare的图片处理API)
const transformed = await fetch(
`https://image.cloudflare.com/width=${width},height=${height},format=${format}/` +
originalResponse.url
);
// 设置缓存
response = new Response(transformed.body, transformed);
response.headers.set('Cache-Control', 'public, max-age=31536000');
// 存入缓存
event.waitUntil(cache.put(cacheKey, response.clone()));
return response;
}
// 其他请求处理
return fetch(request);
}
最后:
避坑指南:血泪换来的经验
- 字体图标陷阱: 改用SVG雪碧图代替iconfont,避免FOIT
- 监控埋点优化: 异步加载统计脚本,防止阻塞
- 骨架屏的正确姿势: 用v-cloak替代第三方库,体积减少90%
- 缓存策略: 强缓存设置过期的惨案——务必用hash文件名!
性能优化的最终目的并不是完全追求时间上的长短,核心目的是给用户更好的体验,在提升了帧数的情况舍弃一点点加载或者渲染时间在整体体验上要比完全追求数值上的长短有意义的多
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 密码箱!
评论