vue客户端渲染首屏优化之道

9,087 阅读10分钟

提取第三方库,缓存,减少打包体积

1、 dll动态链接库, 使用DllPlugin DllReferencePlugin,将第三方库提取出来另外打包出来,然后动态引入html。可以提高打包速度和缓存第三方库 这种方式打包可以见京东团队的gaea方案 www.npmjs.com/package/gae…

2、webpack4的splitChunks或者 webpack3 CommonsChunkPlugin 配合 externals (资源外置) 主要是分离 第三方库,自定义模块(引入超过3次的自定义模块被分离),webpack运行代码(runtime,minifest)。 配合externals,意思将第三方库外置,用cdn的形式引入,可以减少打包体积。 详细代码 在webpack.config.js(peoduction环境下)

externals: {
    'vue': 'Vue', //vue 是包名 Vue是引入的全局变量
    'vue-router': 'VueRouter',
    'vuex': 'Vuex',
    'axios': 'axios',
    'iview': 'iview' //iview
},

然后再main.js或者任何地方不再引入 比如vue,直接使用上面提供的变量

上面没有import vue进来,项目中照常使用Vue这个全局变量。 既然没有import vue 自然不会打包vue,然后你会发现你的vendor.js会从700kb+ 减少到 30-40kb,非常棒的优化。
关于是否注释掉,这里有两张验证图,在webpack配置了externals的情况下
webpack4 splitChunk的配置

//提取node_modules里面的三方模块
module.exports = {
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendor: {
                    chunks: "initial",
                    test: path.resolve(__dirname, "node_modules") // 路径在 node_modules 目录下的都作为公共部分
          name: "vendor", // 使用 vendor 入口作为公共部分
                    enforce: true,
                },
            },
        },
    },
}
//提取 manifest (webpack运行代码)
{
    runtimeChunk: true;
}

webpack3 CommonsChunkPlugin 的配置,写在plugins中

 // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
    // This instance extracts shared chunks from code splitted chunks and bundles them
    // in a separate chunk, similar to the vendor chunk
    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),

下面两个问题详细见 www.jianshu.com/p/23dcabf35…

固定module id,为了缓存

chunk: 是指代码中引用的文件(如:js、css、图片等)会根据配置合并为一个或多个包,我们称一个包为 chunk。 module: 是指将代码按照功能拆分,分解成离散功能块。拆分后的代码块就叫做 module。可以简单的理解为一个 export/import 就是一个 module。
解决方案: HashedModuleIdsPlugin 或者 webpack4 的 optimization.moduleIds='hash'

固定chunk id

我们在固定了 module id 之后同理也需要固定一下 chunk id,不然我们增加 chunk 或者减少 chunk 的时候会和 module id 一样,都可能会导致 chunk 的顺序发生错乱,从而让 chunk 的缓存都失效。
提供了一个叫NamedChunkPlugin的插件,但在使用路由懒加载的情况下,你会发现NamedChunkPlugin并没什么用。
原因: 使用自增 id 的情况下是不能保证你新添加或删除 chunk 的位置的,一旦它改变了,这个顺序就错乱了,就需要重排,就会导致它之后的所有 id 都发生改变了。
下面两种解决方案
第一种:
在 webpack2.4.0 版本之后可以自定义异步 chunk 的名字了,例如:

import(/* webpackChunkName: "my-chunk-name" */ "module");

我们在结合 vue 的懒加载可以这样写。

{
    path: '/test',
    component: () => import(/* webpackChunkName: "test" */ '@/views/test')
  },

还要记得配置chunkFilename

output: {
            path: path.resolve(__dirname, 'dist'),
            publicPath: config.publicPath + '/',//静态文件的处理,生产环境有效.开发环境其实是从内存中拿文件的
            filename: 'js/[name].[chunkhash].js',
            chunkFilename: 'js/[name].[chunkhash].js' //写成[name].xxxx,便于查找chunk源  详细见 NamedChunkPlugin 
        },

打包之后就生成了名为 test的 chunk 文件, chunk 有了 name 之后就可以解决NamedChunksPlugin没有 name 的情况下的 bug 了。查看打包后的代码我们发现 chunkId 就不再是一个简单的自增 id 了。
推荐第一种,既可以固定chunk id(用的chunkname代替),又可以了解项目打包详情比如遇到大文件,到底是哪个chunk出了问题,直接映射问题源

我们可以直接看到786kb的大文件是来自于 test1.vue和test2.vue的vendor包(第三方库),然后进入test1.vue,echarts就是问题源,关于解决就是把echarts等第三方库外置。详细见上面资源外置。

第二种: 原理:根据每个chunk里面的module id 去唯一化这个chunk的name,只要里面的module没有增多或减小,那么它的名字是不会变的

new webpack.NamedChunksPlugin(chunk => {
  if (chunk.name) {
    return chunk.name;
  }
  return Array.from(chunk.modulesIterable, m => m.id).join("_");
});

当然这个方案还是有一些弊端的因为 id 会可能很长,如果一个 chunk 依赖了很多个 module 的话,id 可能有几十位,所以我们还需要缩短一下它的长度。我们首先将拼接起来的 id hash 以下,而且要保证 hash 的结果位数也能太长,浪费字节,但太短又容易发生碰撞,所以最后我们我们选择 4 位长度,并且手动用 Set 做一下碰撞校验,发生碰撞的情况下位数加 1,直到碰撞为止。详细代码如下:

const seen = new Set();
const nameLength = 4;

new webpack.NamedChunksPlugin(chunk => {
  if (chunk.name) {
    return chunk.name;
  }
  const modules = Array.from(chunk.modulesIterable);
  if (modules.length > 1) {
    const hash = require("hash-sum");
    const joinedHash = hash(modules.map(m => m.id).join("_"));
    let len = nameLength;
    while (seen.has(joinedHash.substr(0, len))) len++;
    seen.add(joinedHash.substr(0, len));
    return `chunk-${joinedHash.substr(0, len)}`;
  } else {
    return modules[0].id;
  }
});

提取css为单独文件并压缩

webpack4 的 mini-css-extract-plugin
webpack3的ExtractTextPlugin

压缩js文件

webpack3 UglifyJsPlugin webpack4 自带了UglifyJsPlugin功能,无需配置,需要开启mode production

tree shaking和sideEffects

去除没有被引用的代码, webpack4默认支持。
因为Tree Shaking这个功能是基于ES6 modules 的静态特性检测,来找出未使用的代码,所以如果你使用了 babel 插件的时候,如:babel-preset-env,它默认会将模块打包成commonjs,这样就会让Tree Shaking失效了。

sideEffects是webpack4才有的功能,目的是对第三方没有任何副作用的库进行按需加载。 webpack 的 sideEffects 可以帮助解决这个问题。现在 lodash 的 ES 版本 的 package.json 文件中已经有 sideEffects: false 这个声明了,当某个模块的 package.json 文件中有了这个声明之后,webpack 会认为这个模块没有任何副作用,只是单纯用来对外暴露模块使用,那么在打包的时候就会做一些额外的处理。 例如你这么使用 lodash:


import { forEach, includes } from 'lodash-es'

forEach([1, 2], (item) => {
    console.log(item)
})

console.log(includes([1, 2, 3], 1))

由于 lodash-es 这个模块的 package.json 文件有 sideEffects: false 的声明,所以 webpack 会将上述的代码转换为以下的代码去处理:

import { default as forEach } from 'lodash-es/forEach'
import { default as includes } from 'lodash-es/includes'
// ... 其他代码

最终 webpack 不会把 lodash-es 所有的代码内容打包进来,只是打包了你用到的那两个方法,这便是 sideEffects 的作用。

懒加载 import()

babel需要配置@babel/plugin-syntax-dynamic-import
按需加载 import(/* webpackChunkName: "Index" */ "xxx.vue")
命名设置规则在chunkFilename (如果没有设置,则按照默认的1.xxxx.js这样命名,其实也会分开打包,便于调试,打包时看到某个chunk比较大,可以查看该chunk对应的vue文件) chunkFilename: utils.assetsPath('js/[name].[chunkhash].js') 既然按需加载,就不会打包到 app.js(主entry chunk)中,肯定会分开打包,然后按需加载

babel 按需引入pollyfill

Babel 默认只转换 JavaScript 语法,而不转换新的 API,比如 Promise、Generator、Set、Maps、Symbol 等全局对象,一些定义在全局对象上的方法(比如 Object.assign)也不会被转码。如果想让未转码的 API 可在低版本环境正常运行,这就需要使用 polyfill。

babel6当前最普遍的解决方案

使用transform-runtime或者babel-polyfill
比较transform-runtimebabel-polyfill引入垫片的差异:
使用transform - runtime是按需引入,需要用到哪些polyfill,runtime就自动帮你引入哪些,不需要再手动一个个的去配置plugins,只是引入的polyfill不是全局性的,有些局限性。而且runtime引入的polyfill不会改写一些实例方法,比如Object和Array原型链上的方法,像前面提到的Array.protype.includes

注意使用transform-runtime需要安装babel-runtimebabel-runtime 是一个库,用于引入的 ,放在--save 而 babel-plugin-transform-runtime是帮助引入babel-runtime这个库的(自动的)
babel-runtimebabel-plugin-transform- runtime的区别是,相当一前者是手动挡而后者是自动挡,每当要转译一个api时都要手动加上require('babel-runtime') , 而babel - plugin - transform - runtime会由工具自动添加,主要的功能是为api提供沙箱的垫片方案,不会污染全局的api,因此适合用在第三方的开发产品中。 而重复引入会被webpack设置的commonChunkPlugin 给去重 babel - polyfill就能解决runtime的那些问题,它的垫片是全局的,而且全能,基本上ES6中要用到的polyfill在babel - polyfill中都有,它提供了一个完整的ES6 + 的环境。babel官方建议只要不在意babel - polyfill的体积,最好进行全局引入,因为这是最稳妥的方式。 一般的建议是开发一些框架或者库的时候使用不会污染全局作用域的babel - runtime,而开发web应用的时候可以全局引入babel - polyfill避免一些不必要的错误,而且大型web应用中全局引入babel - polyfill可能还会减少你打包后的文件体积(相比起各个模块引入重复的polyfill来说)。

以下为三种babel6解决ES6 API pollyfill的引入方式
①全局使用babel - polyfill(不设置babel-preset-env options项的useBuiltIns) 具体使用方法如下: a.直接在index.html文件head中直接引入polyfill js或者CDN地址; b.在package.json中添加babel - polyfill依赖, 在webpack配置文件增加入口: 如entry: ["babel-polyfill", './src/app.js'], polyfill将会被打包进这个入口文件中, 必须放在文件最开始的地方; c.在入口文件顶部直接import ''babel-polyfill'; 此方案的优点是简单、一次性可以解决浏览器的所有polyfill兼容性问题,缺点就是一次性引入了ES6 + 的所有polyfill, 打包后的js文件体积会偏大, 在现代浏览器上不需要全部的polyfill, 其次污染了全局对象,不太适合框架类的开发,框架类的开发建议下面的②方案。 注: polyfill.io库会根据你的使用的浏览器做相应的polyfill, 可以极大的解决引入过大的问题。

② 全局使用babel-polyfill(设置babel-preset-env options项的useBuiltIns) 具体使用方法如下:

  1. 引入babel-preset-env包;
  2. 在.babelrc文件预设presets中使用设置babel - preset - env options项 useBuiltins: usage | entry (usage: 仅仅加载代码中用到的 polyfill.entry: 根据浏览器版本的支持,将 polyfill 需求拆分引入,仅引入有浏览器不支持的polyfill) targets.browsers: 浏览器兼容列表 modules: false
  3. 在入口文件顶部直接import ''babel - polyfill';

此方案适合应用级的开发,babel会根据指定的浏览器兼容列表自动引入所有所需的polyfill。

③ 使用插件 babel-runtimebabel-plugin-tranform-runtime babel-runtime会出现重复引用的问题,而babel-plugin-tranform-runtime抽离了公共模块, 避免了重复引入,下面的配置主要以babel-plugin-tranform-runtime来说。

  1. 引入babel-plugin-tranform-runtime包;
  2. .babelrc文件plugins中添加babel-plugin-tranform-runtime: "plugins": ["transform-runtime"];
  3. 配合上面方法②中的第2步中的预设presets的设置;

此方案无全局污染,依赖统一按需引入(polyfill是各个模块共享的), 无重复引入, 无多余引入,适合用来开发库。 安装包

"babel-core": "^6.22.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.1.1",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",

vue-cli的babel-cli的.babelrc


{
    "presets": [
        ["env", {
            "modules": false,
            "targets": {
                "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
            }
        }],
        "stage-2"
    ],
        "plugins": ["transform-vue-jsx", "transform-runtime"]
}

babel7解决方案(注意core-js的版本)

pollyfill 按需加载 @babel/polyfill 模块包括 core-js 和一个自定义的 regenerator runtime 模块用于模拟完整的 ES2015+ 环境。 不再需要手动引入import ''babel-polyfill'; 只需要简单的配置就能自动智能化引入@babel/polyfill,设置useBuiltIns按需加载 .babelrc

{
    "presets": [
        ["@babel/preset-env",
            {
                "modules": false,
                "targets": {
                    "browsers": ["> 1%", "last 2 versions", "not ie <= 8", "Android >= 4", "iOS >= 8"]
                },
                "useBuiltIns": "usage"

            }]
    ],
        "plugins": [
            "@babel/plugin-syntax-dynamic-import"

        ]
}

升级到7需要安装关于@babel的包

"@babel/core": "^7.1.2",
"@babel/plugin-syntax-dynamic-import": "7.0.0", //用于import()
"@babel/polyfill": "7.0.0",
 "@babel/preset-env": "7.1.0",
  "babel-loader": "8.0.4",

babel使用总结,建议使用babel7,构建速度更快,建议使用@babel/preset-env",建议开启useBuiltIns属性,让babel-polyfill按需加载。关于开启与不开启useBuiltIn构建包的大小详细见https://github.com/ab164287643/studyBabel/tree/master/7-babel-env

webpack3和webpack4的差异比较

1、增加了mode配置,只有两种值development | production,对不同的环境他会启用不同的配置。
2、默认生产环境开起了很多代码优化(minify, splite)
3、 开发时开启注视和验证,并加上了evel devtool
4、 生产环境不支持watching,开发环境优化了打包的速度
5、 生产环境开启模块串联(原ModulecondatenationPlugin)
6、自动设置process.env.NODE_EVN到不同环境,也就是不使用DefinePlugin了
7 、如果mode设置none,所有默认设置都去掉了。
8、在webpack4之前,我们处理公共模块的方式都是使用CommonsChunkPlugin,然后该插件的让开发这配置繁琐,并且公共代码的抽离,不够彻底和细致,因此新的splitChunks改进了这些能力。
9、默认开启 uglifyjs - webpack - plugin 的 cache 和 parallel,即缓存和并行处理,这样能大大提高 production mode 下压缩代码的速度。
生产环境和开发环境各自增加很多默认配置(比如UglifyJsPlugin默认用于生产环境),打包速度更快

图片压缩

使用tinify压缩要使用的图片。 详细脚本见 gitee.com/cchennlleii…

关于图片格式优化

jpeg 有损压缩,体积小,不支持透明。
png 无损压缩,高保真,支持透明。
png - 8 2 ^ 8种色彩 256种
png - 24 2 ^ 24种色彩 1600w种
png - 32 2 ^ 24 ^ 8种 (还有8种透明度色彩通道)
颜色支持越多,体积越大
svg 矢量图 体积小 不失真,适用于小图标
base64 减小http请求,但不宜处理大图片,因为大图片增加页面大小,webpack的url - loader已经支持
webP 新兴格式,支持有损和无损压缩,支持透明,体积还特别小,与 PNG 相比,通常提供 3 倍的文件大小,浏览器兼容性低,局限性较大。
项目中的支持webp(参照自京东gaea): 在index.html中判断是否支持webP

window.supportWebp = false;
if (document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') == 0) {
    document.body.classList.add('webp');
    window.supportWebp = true;
}

然后用上面的图片压缩脚本压缩图片,会在img下面生成一个webp文件,里面就是转换后的webp格式的图片。 css中写两套样式,比如

.banner{ background - image: url("xxxx.png") }
.webp .banner{ background - image: url("xxxx.webp") }

在js中根据window.supportWebp去判断用哪种图片。

开启gziped

使用compression - webpack - plugin 在生产环境下开启

if (config.productionGzip) {
    const CompressionWebpackPlugin = require('compression-webpack-plugin');
    //增加浏览器CPU(需要解压缩), 减少网络传输量和带宽消耗 (需要衡量,一般小文件不需要压缩的)
    //图片和PDF文件不应该被压缩,因为他们已经是压缩的了,试着压缩他们会浪费CPU资源而且可能潜在增加文件大小。
    webpackConfig.plugins.push(
        new CompressionWebpackPlugin({
            asset: '[path].gz[query]',
            algorithm: 'gzip',
            test: /\.(js|css)$/,
            threshold: 10240,//达到10kb的静态文件进行压缩 按字节计算
            minRatio: 0.8,//只有压缩率比这个值小的资源才会被处理
            deleteOriginalAssets: false//使用删除压缩的源文件
        })
    )
}

当开启gziped压缩后,服务器需要做相应的配置,让服务器端可以传输压缩后的文件。 开启 nginx 服务端 gzip性能优化。找到nginx配置文件在 http 配置里面添加如下代码,然后重启nginx服务即可。

http: {
    gzip on;
    gzip_static on;
    gzip_buffers 4 16k;
    gzip_comp_level 5;
    gzip_types text / plain application / javascript text / css application / xml text / javascript application / x - httpd - php image / jpeg
    image / gif image / png;
}

开启apache gziped压缩 在 http.conf里面配置 找到下面这句去掉#

LoadModule deflate_module modules / mod_deflate.so

然后在最后面加上,记住不压缩图片

< IfModule mod_deflate.c >
# 告诉 apache 对传输到浏览器的内容进行压缩
SetOutputFilter DEFLATE
# 压缩等级 9
DeflateCompressionLevel 9
#设置不对后缀gif,jpg,jpeg,png的图片文件进行压缩
SetEnvIfNoCase Request_URI.(?: gif | jpe ? g | png)$ no - gzip dont - vary
</IfModule >
可以看到如下效果,http传输大小为173kb,而解压缩后大小为619kb

开启后会大大加快首页加载时长,效果非常不错。

图片懒加载

放个链接吧 juejin.cn/post/684490…

本文所有配置代码
gitee.com/cchennlleii…