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

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

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

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

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

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

上面没有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中html

// 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…vue

固定module id,为了缓存

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

固定chunk id

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

import(/* webpackChunkName: "my-chunk-name" */ "module");
复制代码

咱们在结合 vue 的懒加载能够这样写。webpack

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

还要记得配置chunkFilenameios

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出了问题,直接映射问题源 nginx

咱们能够直接看到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的差别比较

一、增长了mode配置,只有两种值development | production,对不一样的环境他会启用不一样的配置。
二、默认生产环境开起了不少代码优化(minify, splite)
三、 开发时开启注视和验证,并加上了evel devtool
四、 生产环境不支持watching,开发环境优化了打包的速度
五、 生产环境开启模块串联(原ModulecondatenationPlugin)
六、自动设置process.env.NODE_EVN到不一样环境,也就是不使用DefinePlugin了
7 、若是mode设置none,全部默认设置都去掉了。
八、在webpack4以前,咱们处理公共模块的方式都是使用CommonsChunkPlugin,而后该插件的让开发这配置繁琐,而且公共代码的抽离,不够完全和细致,所以新的splitChunks改进了这些能力。
九、默认开启 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.im/post/5bbc60…

本文全部配置代码
gitee.com/cchennlleii…

相关文章
相关标签/搜索