打包Size减小 80%--App 框架优化

486 阅读8分钟

为什么要升级

我们 App 框架是基于vue2+ webpack3 支持国际化的多页面应用,比较通用。文件目录结构大概如下

project/
├── README.md
├── package.json
├── babel.config.js  // Babel配置
├── build/          // webpack配置相关
├── static/          // 静态资源目录
├── src/
│   ├── assets/       // 静态资源文件夹(图片、字体等)
│   ├── components/    // 共享组件
│   ├── pages/         // 多个页面或路由入口
│   │   ├── user/
│   │   │    ├── locales/       // 用户模块相关的国际化语言包
│   │   │    │   ├── en/        // 英语语言包
│   │   │    │   │   └── messages.js
│   │   │    │   ├── zh-CN/     // 简体中文语言包
│   │   │    │   │   └── messages.js
│   │   │    │   └── ...         // 其他语言包
│   │   │    ├── login/
│   │   │    │    ├── index.vue  // 登录页面主组件
│   │   │    │    ├── main.js    // 登录页面入口js
│   │   │    │    └── index.html // (可选)登录页面入口
│   │   │    ├── register/
│   │   │    │    ├── index.vue  // 注册页面主组件
│   │   │    │    ├── main.js    // 注册页面入口js
│   │   │    │    └── index.html // (可选)注册页面入口
│   │   ├── page2/
│   │   └── ...
│   └── utils/              // 工具函数及公用模块
└── .gitignore

框架使用两年后,发现可以从以下方向进一步优化:

  1. 框架不支持新的 ES 语法,比如?.。初步研究方案:需要升级到 babel7,包括它所依赖的 npm 包,那么影响的页面是全量的,全量回归!

  2. 框架构建速度慢。页面数量80个左右,本地服务启动时间约 118 秒,生产构建时间 205 秒。我们另一个项目也是基于此框架,页面数量200个左右,本地服务慢到无法启动。

  3. 页面的公共样式和 js 并没有提取,生产构建包大小 20MB。如果能提取公共文件,比如各页面公用的 vue、vue-i18n 和 lodash 等库可以合并为一个 js 公用。这样构建包会更小,运行时各页面可以共用缓存,同时减少页面首屏渲染时间。

  4. 页面部分静态资源HTTP缓存设置不正确。会导致发版本后文件有更新,用户访问命中到旧的文件。

    1. 有些静态资源比如 style.css,是通引入的,并没有交给 webpack 管理。生产构建后如下
    2. <link rel="stylesheet" href="style.css">
      
    3. 静态资源响应头HTTP缓存设置如下,也就是强制缓存2小时。
    4. Cache-Control:max-age=7200
      
  5. 使用webpack-bundle-analyzer分析构建结果:有冗余的js引入。

  6. 使用Chrome lighthouse分析,建议去掉冗余js和css,优化静态资源加载顺序

  7. static目录下的文件是全部复制到构建目录的,包含了大量无用的文件

  8. i18n文件多个页面共用,打包进每个页面,增加了包大小。

i18n文件放在页面级别

pages/user/login和pages/user/register两个页面用i18n文件是这样写的

module.exports = {
    back: '返回'login: {
        title: '登录'
        ...
    },
    register: {
        title: '注册'
        ...
    }
}

这种目录结构的优点:

  1. login和register页面都共用一些国际化Key,比如"back"
  2. 共用的国际化Key,一处修改,全局生效

缺点也很明显

  1. 很难评估某个国际化Key是否是共用的。勉强用了,需要更新的时候,由于难以评估影响范围,只好在页面再声明一份。

    1. 比如back的值需要改为"取消",影响到的页面都要评估下
  2. 构建包包含的国际化文件冗余。比如login页面包括了语言文件中的register对象。

结论:国际化文件放在页面级别

各页面有独立的locales, 调整目录结构如下:

src/
── pages/
   ├── user/
   │   ├── login/
   │   │    ├── locales/       // 登录页面相关的国际化语言包
   │   │    │   ├── en/        // 英语语言包
   │   │    │   ├── zh-CN/     // 简体中文语言包
   │   │    │   └── ...         // 其他语言包
   │   │    ├── index.vue  // 登录页面主组件
   │   │    ├── main.js    // 登录页面入口js
   │   │    └── (index.html) // (可选)登录页面入口HTML
   │   ├── register/
   │   │    ├── locales/       // 注册页面相关的国际化语言包
  │   │    │   ├── en/        // 英语语言包
  │   │    │   ├── zh-CN/     // 简体中文语言包
  │   │    │   └── ...         // 其他语言包
  │   │    ├── index.vue  // 注册页面主组件
  │   │    ├── main.js    // 注册页面入口js
  │   │    └── (index.html) // (可选)注册页面入口HTML
  └── ...                  // 其他页面或功能模块

login和register有自己独立的locales,优点如下

  1. 修改locales不会影响其他页面

  2. 构建包只包括当前页面下的locales文件

总结

i18n文件放在页面级别,项目构建包大小平均减少30%。

框架升级为Vue-cli

当前框架基于webpack3, 而webpack3 发布于2017年,最新的webpack版本是5。Vue-cli集成了webpack的能力,配置比webpack简单,首先想到的是框架升级为Vue-cli5。

升级步骤

  1. node版本升级16+。因为vue-cli依赖。

  2. 配置文件vue-config.js的主要配置说明:

    1. 页面多入口pages
    2. 提取各页面公共库:vue和vue-i18n为公共js,取名chunk-libs.js
    3. 其他逻辑和原框架webpack配置基本一致。
// 。。。省略处理多页面入口pages

module.exports = defineConfig({
    // ...
    pages, // 多页面入口
    configureWebpack: config => {
        config.resolve = {
            // ...
        };
        config.plugins.push(
            new webpack.DefinePlugin({
            // ...
            })
        );
        if (process.env.NODE_ENV === 'production') {
            config.plugins.push(
                // 复制static目录
                new CopyWebpackPlugin({
                    pattern:[{ from: 'static', to: 'static'}]
                })
            );
            config.plugins.push({
                apply: compiler => {
                    // 构建完成后,处理逻辑
                    compiler.hooks.afterEmit.tap('AfterEmitPlugin', () => {
                        
                    })
                }
            })
        }
    },
    chainWebpack (config) {
        // ... 
        // 提取各页面公共vue和vue-i18n为公共js, 取名为chunk-libs.js
        config.optimization.splitChunks({
            cacheGroups: {
                libs: {
                    name: 'chunk-libs',
                    test: /[\/]node_modules[\/](vue|vue-i18n)/,
                    priority: 10,
                    chunks: 'initial'
                }
            }
        })
    }
})

升级效果

生产构建速度提升约4倍。本地服务启动时间107秒,生产构建时间56秒,包大小17.3MB。

框架升级为Webpack5

为什么要改Vue-cli为webpack5?

  1. 需要修改构建配置提升页面性能,Vue-cli的可定制性不如webpack, 比如: 提取css,压缩css, 去冗余css, 提取公共js等。
  2. Vue-cli相关资料没有webpack丰富
  3. webpack5更流行更通用,易于交流
  4. Vue-cli封装了webpack,webpack更接近原理

升级步骤

  1. 项目结构

    1. 样式文件挪到src/assets/css目录。由页面入口文件main.js引入,这样webpack loader会对样式文件压缩、去冗余处理、提取公共样式文件、生成带hash的文件
        import '@/assets/css/style.css'
        ```
    1.  图片挪到`src/assets/images`目录。这样webpack会压缩图片,并生成带hash的图片
    ```html
        <template>
            <div>
                <img :src="imgUrl"/>
            </div>
        </tempalte>
    
        <script>
        data (){
            imgUrl: require('@/assets/images/banner.png')
        }
        </script>
        ```
    
    
  2. 开发环境webpack.dev.config.js的主要配置说明:

    1. babal-loader配置@babel-preset-env。 后者会根据browserslist,对目标环境作JS语法兼容。这样我们就不用把完整的polyfill全部引入到最终文件里,可以大大减少体积
    2. eslint使用eslint-webpack-plugin
        // 获取页面js入口和html入口, 初始化HTMLWebpackPlugin
        // ...
        module.exports = {
            mode: 'development'stats: 'minimal',
            entry, // 打包入口地址
            output: {
                filename: '[name].js',
                publicPath: '/'
            },
            // ...
            module: {
                rules: [
                    {
                        test: /.js$/i,
                        use: [
                            {
                                loader: 'babel-loader',
                                options: {
                                    presets: ['@babel/preset-env']    
                                }
                            }
                        ]
                    }
                ]
            },
            plugins: [
                new EslintWebpackPlugin({
                //...
                }),
                new CopyWebpackPlugin({
                    patterns: [{ from: 'static', to: 'static'}]
                }),
                // htmlWebpackPlugin包含js和html入口
                ...htmlWebpackPluginList,
                new VueLoaderPlugin(),
                // ...
            ]
        }
        ```
    
    
  3. 生产环境webpack.prod.config.js,主要配置说明

    1. 用webpack默认splitChunks方式进行代码分割
    2. 使用TerserPlugin进行js压缩
    3. 提取.vue文件中的样式生成css,由splitChunks进行分割
    4. 提取的js、css、和图片都添加contenthash。文件内容有变化时,contenthash才会变化。这样有利于及时更新缓存,又高效利用缓存。
        // 获取页面js入口及html入口,并初始化HTMLWebpackPlugin
        // ...
        const webpackConfig = {
            mode: 'production',
            stats: 'minimal',
            entry, //打包入口地址
            output: {
                clean: true,
                filename: 'static/js/[name].[contenthash:8].js', // 输入js文件名
                path:  path.join(__dirname, `../dist`) // 输入文件目录
            },
            devtool: false,
            resolve: {
                // ...
            },
            module:{
                rules: [
                    // ...
                ]
            },
            optimization: {
                minimize: true,
                splitChunks:{
                    // 表示选择哪些chunks进行分割,可选值有async,initial和all
                    chunks: 'all'
                },
                minimizer: [
                    // 添加css压缩配置
                    new CssMinimizerPlugin(),
                    new TerserPlugin({
                        parallel: true, // 启用多进程
                        exclude: /node_modules/, // 匹配不需要压缩的文件
                        extractComments: false, //不提取注释
                        terserOptions: {
                            compress:{
                                arguments: true,
                                dead_code: true,
                                drop_console: true,
                                drop_debugger: true,
                            },
                            format:{
                                comments: false // 删除注释
                            }
                        }
                    })
                ]
            },
            plugins: [
                new CopyWebpackPlugin({
                    patterns: [{ from: 'static', to: 'static'}]
                }),
                // htmlWebpackPlugin包含js和html入口
                ...htmlWebpackPluginList,
                // 抽取css样式,放在static/css目录下
                new MiniCssExtractPlugin({
                    filename: 'static/css/[name].[contenthash:9].css'
                })
                new VueLoaderPlugin(),
                // ...
            ]
        }
        ```
    

升级效果

本地服务启动速度提升3倍,生产构建时间提升5倍,生产构建包大小减小60%。

本地服务启动时间36秒,生产构建时间44秒,包大小7.1MB。

经验总结

  1. 升级过程中比较多时间花在解决npm包冲突上

    1. chalk报错。是因为版本过高,ES6无法解析。使用chalk@4.1.2版本
    2. 解析vue2的vue-loader版本15.x
  2. Webpack cache设置filesystem的确可以大幅非首次的启动速度,但是修改package.json相关的环境变量后,并不生效。故不再使用。

  3. purgecss-webpack-plugin用于删除无用css,但实测会莫名删除有用的样式。故不使用。

进一步优化本地服务

  1. Eslint检查加入缓存设置
  2. 本地服务启动,babel不必兼容低版本浏览器
  3. 本地服务启动,postcss-loader样式兼容不需要
new ESLintWebpackPlugin({
    // ...
    cache: true
})
// package.json文件
"broswerslist": {
    "development": [
        "chrome>100"
    ],
    "production": [
       // ...
    ]
}

升级效果

本地服务启动时间18秒, 热更新时间3秒

Webpack Analyzer分析优化

此处图略去

  1. 模块中字符串使用+方式。因为模板字符串中的表达式是动态解析的,tree shaking不生效。
// 模板字符串方式
export const url = `${baseUrl}/app/user/login`
// 字符串+方式
export const url = baseUrl + '/app/user/login'
  1. 其中一处时间久远的库文件使用lodash, 仅此一处使用lodash。因此简化此方法,删除lodash.js

webpack在生产模式下默认tree shaking所有未标记为副作用的代码。

Chrome LightHouse分析优化

此处图略去

  1. ui库js和css阻塞渲染,最好使用模块化方式引入。据可考经验,ui库不支持。

    1. 这样webpack可以理清依赖,首屏渲染时最小引入。
// html引入
<link src="ui.css">
<script src="ui.js">
// js模块引入
import 'ui.css'
import 'ui.js'
// 保证ui库初始化时有vue.js
UI.use(Vue)
  1. 其他第三方js,如native jsbridge、事件上报等,均不支持npm模块化

总结

  1. 框架优化大幅提升了构建速度、减小了包大小。主要原因在于webpack5优秀的性能。
  2. 首屏渲染进步不大,主要是因为第三方js阻塞沉浸,并不支持npm模块。

webpack5为何如此优秀,通义千问答:

Webpack 5在性能优化方面相较于之前的版本有显著提升,主要体现在以下几个方面:

  1. 持久化缓存(Persistent Caching): Webpack 5 引入了更强大的持久化缓存机制,默认配置下即可实现更好的缓存效果。这有助于在多次构建之间重用已编译的结果,从而极大地提高开发环境和持续集成/部署环境下的构建速度。
  2. Tree Shaking 优化: Tree Shaking 是指去除未使用的模块代码以减小包体积的过程。Webpack 5 在这一方面进行了改进,使得对ES模块的静态分析更加精确,能更有效地移除无用代码。
  3. 代码生成与压缩优化: Webpack 5 改进了其代码生成器和其他相关工具,如引入css-minimizer-webpack-plugin替代旧版插件,它提供更好的CSS压缩性能。同时,通过与其他新的或更新的加载器配合,可以更高效地处理资源文件,比如图片、字体等。
  4. 内存管理改进: Webpack 5 对内存使用进行了优化,减少内存泄漏的风险,并提高了大型项目中构建时的内存效率。
  5. API兼容性调整: 清除了之前遗留下来的为了向后兼容而存在的部分API,这些API可能会影响整体性能。去掉这些冗余后,Webpack 能够运行得更为轻量化和高效。
  6. 模块解析优化: Webpack 5 提高了模块解析的速度,减少了构建过程中的瓶颈。
  7. 内置性能提示: Webpack 5 内置了一些功能来帮助开发者更好地识别构建过程中的性能问题,例如,提供了更多的性能提示和日志信息。

综上所述,Webpack 5 的性能优秀是由于它在多个关键领域实现了深度优化,包括但不限于构建时间、资源处理效率、内存利用率以及整体架构的精简和现代化。这些改进为开发者带来了更快的构建速度、更小的产出体积以及更流畅的开发体验。