微应用性能优化
背景
最近腾出手来对公司平台的子应用进行性能优化,因为在第一次访问子应用时会白屏好几秒,用户体验很不好。
问题调研
通过谷歌浏览器自带性能分析工具(Lighthouse)检查发下问题如下图:
FCP 问题如下图:
正常指标参考:
优化方案
- 构建工具优化配置
- 消除阻塞页面的静态资源(CSS,JS),
- 去除剔除未使用的代码(CSS, JS)
- 对过大的文件进行压缩
- 请求优化: 使用 HTTP2 提高资源加载速度 -- 目前平台不支持HTTPS 暂未实现
- 代码优化:开发过程中注意减少重绘重排的代码
具体优化点
- 代码优化
- app.css 打包出来的内容过大 ,拆分 app.css 减少体积,剔除无用的代码.
- 使用 Webpack Webpack Bundle Analyzer 分析打包之后的文件,识别未使用代码
- 使用TerserPlugin 压缩和移除未使用的 JavaScript 代码
- 使用 PurgeCSS 移除未使用的 CSS 代码
- MiniCssExtractPlugin 提取 CSS 文件
- 服务端开启 HTTP2.0 的支持
- 图片设置具体的宽高减少浏览器计算
- 服务器开启gzip 压缩,修改 Nginx 配置nginx
user nginx; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; #新增开始 gzip_static on; # 启用预压缩文件支持 gzip_comp_level 5; # 压缩级别 (1-9) gzip_min_length 1024;# 最小压缩文件大小 1kb gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript gzip_types font/ttf font/otf; #压缩类型 gzip_vary on; #添加vary响应头 open_file_cache max=1000 inactive=20s; # 缓存 1000 个最常访问资源,20秒内未被访问自动释放 open_file_cache_valid 30s; # 30 秒验证一次图片是否被更新 open_file_cache_min_uses 2; # 只有被访问 2 次以上的图片才会进入缓存 # 新增结束 include /etc/nginx/conf.d/*.conf; }
- 优化 Webpack 打包配置js
const Path = require('path'); const IconfontPlugin = require('webpack-iconfont-plugin-nodejs'); const CompressionWebpackPlugin = require('compression-webpack-plugin'); const { SvgChainConfig } = require('@hatech/icon/src/utils'); const iconDir = 'static/icons'; const iconOutDir = 'src/assets/icon'; const productionGzipExtensions = ['js', 'css']; const { packageName } = require('./package.json'); const devApiUrl = `${process.env.VUE_APP_URL}/`; const filePublicPath = process.env.NODE_ENV === 'production' ? packageName : ''; module.exports = { publicPath: `/${packageName}/`, outputDir: 'dist', devServer: { hot: true, port: 9999, compress: false, headers: { 'Access-Control-Allow-Origin': '*' }, proxy: { '/api': { target: devApiUrl, ws: true, changeOrigin: true, pathRewrite: { '^/': '/' } }, // ws代理 '/ws': { target: devApiUrl, ws: true } } }, configureWebpack: { resolve: { alias: { '@': Path.resolve(__dirname, 'src'), }, }, module: { rules: [{ test: /\.mjs$/, include: /node_modules/, type: 'javascript/auto' }] }, output: { library: `system-[${packageName}]`, libraryTarget: 'umd', chunkLoadingGlobal: 'webpackJsonp_system', filename: 'js/[name].[contenthash:8].js', chunkFilename: 'js/[name].[contenthash:8].chunk.js' }, plugins: [ new IconfontPlugin({ fontName: `iconfont_${packageName}`, cssPrefix: 'ha-icon', svgs: Path.join(iconDir, 'svg/*.svg'), fontsOutput: iconOutDir, cssOutput: Path.join(iconOutDir, 'font.css'), htmlOutput: Path.join(iconOutDir, 'preview.html'), // jsOutput: Path.join(iconOutDir, 'fonts.js'), formats: ['ttf', 'woff', 'woff2'] }), new CompressionWebpackPlugin({ filename: '[path].gz[query]', // 提示compression-webpack-plugin@3.0.0的话asset改为filename algorithm: 'gzip', test: new RegExp(`\\.(${productionGzipExtensions.join('|')})$`), threshold: 10240, minRatio: 0.8 }) ], optimization: { runtimeChunk: { name: 'runtime' }, splitChunks: { chunks: 'all', // 包大小2M maxSize: 1024 * 1024 * 1.5, minSize: 1024 * 30, maxAsyncRequests: 6, maxInitialRequests: 4, minChunks: 2, cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name(module) { const name = module.context.match( /[\\/]node_modules[\\/](.*?)([\\/]|$)/ )[1]; return `vendor.${name.replace('@', '')}`; // 按包名独立分包 }, priority: 10, // 优先级高于默认组 chunks: 'all' }, common: { // 提取公共模块 minChunks: 2, name: 'common', chunks: 'initial', priority: 5 }, // 异步加载优化 async: { chunks: 'async', minSize: 30000, maxSize: 150000, name: 'async-chunks' }, // 拆分为现代/传统浏览器包 modern: { test: /[\\/]node_modules[\\/](core-js|@babel|regenerator-runtime)/, name: 'modern-vendor', chunks: 'all', priority: 20 } } } }, }, chainWebpack: (config) => { // svg loader设置 const svgRule = config.module.rule('svg'); // 清除已有的所有 loader。 // 如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。 svgRule.uses.clear(); const fileRule = config.module.rule('file'); fileRule.uses.clear(); fileRule .test(/\.svg$/) .exclude.add(Path.resolve(__dirname, './src/icons')) .end() .use('file-loader') .loader('file-loader') .options({ publicPath: filePublicPath }) .end(); config.module.rule('fonts').type('asset') .set('generator', { filename: 'fonts/[name].[hash:8][ext]', publicPath: filePublicPath }); // 加载公司图标 SvgChainConfig(config, { path: './src/icons' }); }, css: { loaderOptions: { sass: { api: 'modern' } } } };
优化后结果
CSS 文件
分包之前:
分包之后:
Javascirpt 文件
分包之前:
分包之后:
优化后通过谷歌浏览器自带性能分析工具(Lighthouse)检查如下图:
指标对比表:
名称 | 优化前时间(秒) | 优化后时间(秒) |
---|---|---|
First Contentful Paint(首次内容绘制) | 1.7 | 0.7 |
Largest Contentful Paint (用户看到主要内容的时间) | 4.5 | 2 |
Total Blocking Time (主线程阻塞时间) | 0.76 | 0.63 |
Cumulative Layout Shift (页面内容稳定时间) | 0.1 | 0.035 |
总结
- 目前的 gzip 的配置是针对平台所有的,主应用和子应用。
- webpack 的配置修改目前只在子应用上
- 优化并没有到极致,代码里还存一些未使用到的 js,css 这些需要后续进行处理
- 还存在一些 js较大 阻塞了主线程。需要后续优化代码和进行文件内容拆分