前端性能优化实用方案(一):减少50%首屏资源体积的Webpack配置
1 首屏加载慢?试试这几个减少资源体积的方法
做前端的都知道,首屏加载速度直接关系到用户会不会继续用你的产品。有数据显示,页面加载时间每增加1秒,转化率就会下降7%。
这个数字听起来很抽象,但想想你自己平时的使用习惯就明白了——打开一个网页,如果3秒还没加载出来,你是不是就想关掉了?我在实际项目中验证过这些方法,效果很明显。
本文重点讲如何通过减少首屏资源体积来提升页面加载性能。
1.1 减少首屏资源体积的核心思路
首屏资源体积是影响加载速度的关键因素。我们可以从几个维度来优化,每个维度都能带来不同程度的提升。
1.2 打包工具的压缩配置
现代打包工具的压缩能力很强,但很多人都没有充分利用。合理配置可以让bundle体积减少30-50%。
Webpack压缩配置
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除console
drop_debugger: true, // 移除debugger
pure_funcs: ['console.log'] // 移除指定函数
},
mangle: true, // 混淆变量名
},
extractComments: false, // 不提取注释到单独文件
})
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
}
}
}
},
plugins: [
// Gzip压缩
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
})
]
};
Vite压缩配置
Vite的配置相对简单一些,但效果同样出色:
import { defineConfig } from 'vite';
import { resolve } from 'path';
import viteCompression from 'vite-plugin-compression';
export default defineConfig({
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
utils: ['lodash', 'axios']
}
}
}
},
plugins: [
viteCompression({
algorithm: 'gzip',
ext: '.gz'
})
]
});
这套配置在我的项目中通常能减少30-50%的bundle体积。特别是开启Gzip后,文本文件的压缩率能达到70%以上。
1.3 异步加载:让首屏只加载必要的内容
异步加载的核心思想是:首屏只加载用户立即需要看到的内容,其他的延后加载。
路由懒加载
这是最基础也是最有效的优化方式:
import { createRouter, createWebHistory } from 'vue-router';
// 传统同步加载(不推荐)
// import Home from '../views/Home.vue';
// import About from '../views/About.vue';
const routes = [
{
path: '/',
name: 'Home',
// 路由懒加载
component: () => import('../views/Home.vue')
},
{
path: '/about',
name: 'About',
// 使用webpack魔法注释指定chunk名称
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
// 预加载:在空闲时间预先加载
component: () => import(/* webpackChunkName: "dashboard", webpackPrefetch: true */ '../views/Dashboard.vue')
}
];
export default createRouter({
history: createWebHistory(),
routes
});
组件懒加载
对于页面内的非关键组件,也可以采用懒加载:
<template>
<div>
<h2>首屏内容</h2>
<!-- 首屏可见内容 -->
<div class="above-fold">
<p>重要内容立即显示</p>
</div>
<!-- 懒加载组件 -->
<Suspense>
<template #default>
<AsyncChart v-if="showChart" :data="chartData" />
</template>
<template #fallback>
<div class="loading">图表加载中...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 异步组件
const AsyncChart = defineAsyncComponent({
loader: () => import('./Chart.vue'),
delay: 200,
timeout: 3000,
errorComponent: () => import('./ErrorComponent.vue'),
loadingComponent: () => import('./LoadingComponent.vue')
});
const showChart = ref(false);
const chartData = ref([]);
// 延迟加载非关键内容
onMounted(() => {
// 首屏渲染完成后再加载图表
setTimeout(() => {
showChart.value = true;
loadChartData();
}, 100);
});
const loadChartData = async () => {
// 异步加载数据
const { default: chartModule } = await import('../utils/chartData.js');
chartData.value = chartModule.getData();
};
</script>
图片懒加载
图片往往是页面中最大的资源,懒加载效果特别明显:
// 原生Intersection Observer实现
class LazyLoader {
constructor() {
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
rootMargin: '50px 0px', // 提前50px开始加载
threshold: 0.1
}
);
}
observe(element) {
this.observer.observe(element);
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.dataset.src;
if (src) {
img.src = src;
img.removeAttribute('data-src');
this.observer.unobserve(img);
}
}
});
}
}
// 使用示例
const lazyLoader = new LazyLoader();
// 为所有懒加载图片添加观察
document.querySelectorAll('img[data-src]').forEach(img => {
lazyLoader.observe(img);
});
1.4 升级到体积更小的新版本
很多第三方库在新版本中都会优化体积,支持更好的tree-shaking。定期检查依赖版本是个好习惯。
xlsx库优化案例
这是一个典型的例子,新版本的xlsx库体积减少了60%:
// 旧版本(不推荐)- xlsx@0.12.x
// import XLSX from 'xlsx'; // 整个库约600KB
// 新版本(推荐)- xlsx@0.18.x+
import { read, write, utils } from 'xlsx'; // 按需引入,减少约60%体积
// 进一步优化:只引入需要的功能
import { read } from 'xlsx/dist/xlsx.mini.min.js'; // 最小化版本
class ExcelHandler {
// 读取Excel文件
static async readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = read(data, { type: 'array' });
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = utils.sheet_to_json(worksheet);
resolve(jsonData);
} catch (error) {
reject(error);
}
};
reader.readAsArrayBuffer(file);
});
}
// 导出Excel文件
static exportToExcel(data, filename = 'export.xlsx') {
const worksheet = utils.json_to_sheet(data);
const workbook = utils.book_new();
utils.book_append_sheet(workbook, worksheet, 'Sheet1');
// 使用动态导入减少初始bundle体积
import('xlsx').then(XLSX => {
XLSX.writeFile(workbook, filename);
});
}
}
export default ExcelHandler;
日期库优化
moment.js虽然功能强大,但体积太大了。day.js是个很好的替代方案:
// 替换moment.js(约67KB)为day.js(约2KB)
// import moment from 'moment'; // 不推荐
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import utc from 'dayjs/plugin/utc';
// 按需加载插件
dayjs.extend(relativeTime);
dayjs.extend(utc);
export const formatDate = (date, format = 'YYYY-MM-DD') => {
return dayjs(date).format(format);
};
export const getRelativeTime = (date) => {
return dayjs(date).fromNow();
};
// 体积对比:
// moment.js: ~67KB (gzipped: ~22KB)
// day.js: ~2KB (gzipped: ~1KB)
// 减少约97%的体积
这个替换在我的项目中节省了65KB的体积,而且API几乎一样,迁移成本很低。
1.5 能自己实现就不用第三方库
对于一些简单功能,自己实现往往比引入整个库更划算。
自实现常用工具函数
lodash很好用,但如果你只用几个函数,完全可以自己实现:
// 替代lodash的常用函数
// 防抖函数 - 替代lodash.debounce
export const debounce = (func, wait, immediate = false) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
timeout = null;
if (!immediate) func.apply(this, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(this, args);
};
};
// 节流函数 - 替代lodash.throttle
export const throttle = (func, limit) => {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
};
// 深拷贝 - 替代lodash.cloneDeep
export const deepClone = (obj) => {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof Array) return obj.map(item => deepClone(item));
if (typeof obj === 'object') {
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj;
}
};
// 数组去重 - 替代lodash.uniq
export const unique = (arr) => [...new Set(arr)];
// 对象属性获取 - 替代lodash.get
export const get = (obj, path, defaultValue = undefined) => {
const keys = path.split('.');
let result = obj;
for (const key of keys) {
if (result == null || typeof result !== 'object') {
return defaultValue;
}
result = result[key];
}
return result !== undefined ? result : defaultValue;
};
// 体积对比:
// lodash完整版: ~70KB (gzipped: ~25KB)
// 自实现常用函数: ~2KB (gzipped: ~1KB)
// 减少约96%的体积
简单动画替代方案
如果只是做一些简单的动画效果,没必要引入整个动画库:
// 替代复杂动画库的轻量级解决方案
// 简单的缓动函数
const easing = {
linear: t => t,
easeInQuad: t => t * t,
easeOutQuad: t => t * (2 - t),
easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
};
// 通用动画函数
export const animate = ({
from,
to,
duration = 300,
easeType = 'easeOutQuad',
onUpdate,
onComplete
}) => {
const startTime = performance.now();
const easeFn = easing[easeType] || easing.linear;
const step = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easedProgress = easeFn(progress);
const currentValue = from + (to - from) * easedProgress;
onUpdate(currentValue);
if (progress < 1) {
requestAnimationFrame(step);
} else {
onComplete && onComplete();
}
};
requestAnimationFrame(step);
};
// 使用示例
// animate({
// from: 0,
// to: 100,
// duration: 500,
// onUpdate: (value) => {
// element.style.opacity = value / 100;
// }
// });
1.6 编写代码时就考虑体积
写代码的时候稍微注意一下,就能减少不少体积。
代码优化技巧
// 1. 使用更短的变量名(在压缩前)
// 不推荐
const userInformationData = getUserData();
const processedUserInformation = processUserData(userInformationData);
// 推荐
const userData = getUserData();
const processed = processUserData(userData);
// 2. 避免重复代码,提取公共函数
// 不推荐
const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const validatePhone = (phone) => {
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(phone);
};
// 推荐
const createValidator = (regex) => (value) => regex.test(value);
const validateEmail = createValidator(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
const validatePhone = createValidator(/^1[3-9]\d{9}$/);
// 3. 使用对象解构减少重复访问
// 不推荐
const processUser = (user) => {
console.log(user.name);
console.log(user.email);
console.log(user.age);
return {
displayName: user.name,
contact: user.email,
years: user.age
};
};
// 推荐
const processUser = ({ name, email, age }) => {
console.log(name, email, age);
return {
displayName: name,
contact: email,
years: age
};
};
// 4. 使用模板字符串替代字符串拼接
// 不推荐
const createMessage = (name, action, time) => {
return 'User ' + name + ' performed ' + action + ' at ' + time;
};
// 推荐
const createMessage = (name, action, time) => `User ${name} performed ${action} at ${time}`;
// 5. 合理使用三元运算符
// 不推荐
let status;
if (user.isActive) {
status = 'active';
} else {
status = 'inactive';
}
// 推荐
const status = user.isActive ? 'active' : 'inactive';
Tree Shaking优化
确保你的代码支持Tree Shaking,这样打包工具就能自动去除未使用的代码:
// 确保代码支持Tree Shaking
// 1. 使用ES6模块语法
// 推荐
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
// 不推荐
// module.exports = {
// add: (a, b) => a + b,
// subtract: (a, b) => a - b,
// multiply: (a, b) => a * b
// };
// 2. 避免导入整个模块
// 不推荐
import * as utils from './utils';
utils.add(1, 2);
// 推荐
import { add } from './utils';
add(1, 2);
// 3. 避免副作用
// 不推荐(有副作用,无法被tree shake)
console.log('Module loaded');
export const helper = () => {};
// 推荐(纯函数,可以被tree shake)
export const helper = () => {};
1.7 去除大的base64体积
base64编码会让文件体积增加33%,对于大文件来说这个开销很大。
图片资源优化
// 1. 避免大图片的base64编码
// 不推荐
const largeImageBase64 = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ...' // 几百KB的base64
// 推荐:使用CDN或静态资源
const imageUrl = 'https://cdn.example.com/images/large-image.webp';
// 2. 小图标可以考虑base64(<2KB)
const smallIcon = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQi...';
// 3. 动态加载图片
const loadImage = (src) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
};
// 4. 图片压缩和格式优化
const optimizeImage = async (file, quality = 0.8) => {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
// 计算压缩后的尺寸
const maxWidth = 1920;
const maxHeight = 1080;
let { width, height } = img;
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
if (height > maxHeight) {
width = (width * maxHeight) / height;
height = maxHeight;
}
canvas.width = width;
canvas.height = height;
// 绘制并压缩
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(resolve, 'image/webp', quality);
};
img.src = URL.createObjectURL(file);
});
};
Webpack配置优化
在webpack配置中,可以设置只有小于2KB的图片才转为base64:
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 2 * 1024 // 只有小于2KB的图片才转为base64
}
},
generator: {
filename: 'images/[name].[hash:8][ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash:8][ext]'
}
}
]
}
};
小结
通过这六个方面的优化,我们可以大幅减少首屏资源体积:
- 打包工具压缩:减少30-50%的bundle体积
- 异步加载:减少首屏阻塞时间50-70%
- 库版本更新:减少60-90%的第三方库体积
- 自实现替代:减少90%以上的不必要依赖
- 代码优化:减少10-20%的代码体积
- base64优化:减少33%的图片传输体积
这些优化措施的综合效果通常可以将首屏资源体积减少60-80%,页面加载速度提升明显。
在实际项目中,我建议按照优先级来实施:先做异步加载和打包压缩,这两个效果最明显;然后考虑替换大体积的第三方库;最后再优化代码细节。
下一篇文章我们会讲首屏速度优化的其他方面,包括资源加载策略和缓存优化。
- 点赞
- 收藏
- 关注作者
评论(0)