Skip to content

微应用性能优化

背景

最近腾出手来对公司平台的子应用进行性能优化,因为在第一次访问子应用时会白屏好几秒,用户体验很不好。

问题调研

通过谷歌浏览器自带性能分析工具(Lighthouse)检查发下问题如下图:

性能指标

FCP 问题如下图:

FCP问题

正常指标参考:

FCP正常指标

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 文件

分包之前:

css分包之前

分包之后:

css分包之后

Javascirpt 文件

分包之前:

js分包之前

分包之后:

js分包之后

优化后通过谷歌浏览器自带性能分析工具(Lighthouse)检查如下图:

FCP优化之后

指标对比表:

名称优化前时间(秒)优化后时间(秒)
First Contentful Paint(首次内容绘制)1.70.7
Largest Contentful Paint (用户看到主要内容的时间)4.52
Total Blocking Time (主线程阻塞时间)0.760.63
Cumulative Layout Shift (页面内容稳定时间)0.10.035

总结

  1. 目前的 gzip 的配置是针对平台所有的,主应用和子应用。
  2. webpack 的配置修改目前只在子应用上
  3. 优化并没有到极致,代码里还存一些未使用到的 js,css 这些需要后续进行处理
  4. 还存在一些 js较大 阻塞了主线程。需要后续优化代码和进行文件内容拆分