前端性能优化实用方案(一):减少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, // 移除consoledrop_debugger: true, // 移除debuggerpure_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%,页面加载速度提升明显。
在实际项目中,我建议按照优先级来实施:先做异步加载和打包压缩,这两个效果最明显;然后考虑替换大体积的第三方库;最后再优化代码细节。
下一篇文章我们会讲首屏速度优化的其他方面,包括资源加载策略和缓存优化。