当前位置: 代码迷 >> 综合 >> Webpack的进阶概念-CodeSplitting、懒加载与预加载
  详细解决方案

Webpack的进阶概念-CodeSplitting、懒加载与预加载

热度:71   发布时间:2024-01-14 12:33:41.0

文章目录

  • 1. CodeSplitting
    • 1.1 问题引入
    • 1.2 引入codeSplitting解决
    • 1.3 webpack中的CodeSplitting
    • 1.4 异步模块引入和拆分
    • 1.5 SplitChunksPlugin配置参数详解
      • 重命名
      • 针对异步/同步代码分割
      • 代码分割的阈值
      • 二次分割
      • 打包数量要求
      • 命名连接符
      • cacheGroups
    • 1.6 Chunk是什么
  • 2. 懒加载LazyLoading
  • 3. 打包分析工具
  • 4. 预加载
    • 4.1 问题引入
    • 4.2 Preloading与Prefetching解决问题

1. CodeSplitting

这里主要讲解一下webpack与代码分割的关系以及相关配置项

1.1 问题引入

首先我们尝试引入常用的第三方库:lodash

npm install lodash --save

接着我们去index.js里面引入这个库:

//index.js
import _ from 'lodash';
console.log(_.join(['a','b','c'],'***'))

打包之后发现可以正常打印log

现在我们去看一下main.js,发现目前所有的业务代码和lodash工具代码都打包在了一起。

真正情况下可能业务代码非常的多,如果现在这样打包会把两者打包到一起,从代码运行层面上来看没有问题。但是有一个潜在的问题,假设lodash很大,有1MB,业务代码也有1MB,假设不压缩main.js有2MB,如果用户访问index.js的页面就需要等待2MB加载完之后才能看到…

一方面导致打包文件太大,加载时间太长,影响体验

另一方面其实lodash库的代码其实不会变化,但是当业务代码改动之后,如果用户重新访问页面,又必须重新加载2MB的包

1.2 引入codeSplitting解决

这里我们能不能解决上述问题呢?一个方向是把不变的库代码抽取出来,不希望每一次都重新加载。

首先我们在源代码新建lodash.js,尝试把lodash的引用放在这里:

import _ from 'lodash';
window._ = _;

我们将lodash入口挂在到全局变量上,这样index.js不需要引入lodash了

接着我们给webpack加一个配置项:

module.exports = {entry:{lodash:'./src/lodash.js',main:'./src/index.js'}
}

然后重新打包,会发现dist目录下有两个bundle文件,lodash.js和main.js,同时index.html使用两个script标签同时引入了他们

此时main.js被拆分了,变成两个1MB的文件,浏览器可以异步加载,这样就会比加载一个2MB文件加载快一点。

同时这种方式如果只是业务逻辑变更,只需要重新加载main.js,lodash.js没有改变通过缓存不需要再加载!

项目中我们会通过对代码公用部分的拆分来提高加载速度。而这种代码的拆分就是Code Splitting核心概念 — 通过拆分提高执行性能和用户体验。

1.3 webpack中的CodeSplitting

代码分割这个概念早在webapck出现之前就有,但是webpack为了更方便的实现该优化提供了很多插件来简化操作。

甚至不需要我们之前手动进行代码分割,可以利用内置插件进行自动代码分割。这里我们来看看应该如何配置,我们还原之前的index.js,引入lodash

接着尝试在配置文件里面配置相关内容:

module.exports = {...optimization:{splitChunks:{chunks:'all'}}
}

接着重新打包,我们发现打包后的main.js里面没有lodash相关内容,但是新生成的vendors~main.js里面有

这里通过一个很简单的配置就把工具类库代码智能分割了,所以这个功能其实还是很强大的。

1.4 异步模块引入和拆分

除了上述配置项使用之外,还有其他方式可以实现code splitting,其实上述index.js中的引入和使用lodash是一个同步过程,实际上除了同步模块引入,还可以做异步模块的引入:

function getComponent(){return import('lodash').then(() => {...do something})
}

上述写法中lodash只会在调用该函数时通过jsonp的形式来调用加载

什么是JSONP?

加载完成之后then方法会执行,一般我们会这么写,下面举了个例子利用lodash动态生成一个element:

function getComponent(){return import('lodash').then(({default:_}) => {var element = document.createElement('div');element.innerHTML = _.join(['dell','lee'],'-'))return element;})
}getComponenet().then(element => {document.body.appendChild(element);
})

上述代码允许时会报错,大体意思是动态引入目前是实现性性质,还不能用,这里可以通过一个babel插件做一下转换:

npm install babel-plugin-dynamic-import-webpack --save-dev

babel配置文件修改一下,给plugins数组加入[“dynamic-import-webpack”]

接着重新打包会发现有一个0.js文件,里面的就是lodash内容,会在调用对应函数的时候加载

现在我们清楚webpack实现code Splitting两者方式:

  1. 借助webapck的配置和同步代码分析完成
  2. 异步加载模块的拆分

1.5 SplitChunksPlugin配置参数详解

上述代码分割webpack底层主要基于该插件进行,这里在说明一些常用的配置参数

重命名

之前我们把异步加载的模块单独打成了一个0.js,如果想重命名可以这样做:

首先通过代码注释来这样写:

function getComponent(){return import(/* webpackChunkName:"lodash"*/ ...)
}

接着因为我们之前使用的dynamic-import-webpack不支持这种语法,可以换成官方的动态模块插件:plugin-syntax-dynaminc-import

然后重新打包即可,这里会发现有一个vendors前缀,如果你想要去掉就需要配置splitChunks配置项:

optimization:{...splitChunks:{chunks:all,cacheGroups:{vendors:false,default:false}}
}

再打包就可以了,无论是同步还是异步分割,都可以通过该配置项完成

针对异步/同步代码分割

chunks:'async' //只对异步代码生效

对于同步的代码就不进行代码的分割了,如果你想都做代码分割,一般配置为‘all’就可以了

如果是同步代码分割还需要配置cacheGroups配置项,把分割的部分分组打包,例如下写法:

cacheGroups:{vendors:{test:/[\\/]node_modules[\\/]/,priority:-10,filename:'vendros.js'},default:false
}

引入lodash库时,会先检测是否在node_modules中,如果在就会单独打包到vendors组中,同时bundle名统一为vendors.js

代码分割的阈值

minSize: 30000 // 引入的模块或库大于30KB才会做代码分割,如果小于就不做代码分割

lodash库很大,所以会帮你做代码分割。一般这个配置项的大小不用变

minChunks:1 //至少被引入1次才会做代码分割

二次分割

maxSize:0

例如我们改为50000,那么针对大小1MB的lodash库,他会尝试进行二次分割来能分成多个50KB的包,但是像lodash这种基本上拆不了的,打包出来还是个1MB的库

一般不大使用

打包数量要求

maxAsyncRequests:5 //最多同时加载5个模块,多了就不进行分割了

如果你分的包很多,由10个JS库分割的代码,那么你一打开网页就需要加载很多包,不一定会有性能提升。

上述配置只有打包前5个库会生成js文件,超过就不会做了。

maxInitialRequests:3

命名连接符

automaticNameDlimiter:"~"

就是之前组和名称连接的连接符,之前都是~号

cacheGroups

cacheGroups:{vendors:{test:/[\\/]node_modules[\\/]/,priority:-10,filename:'vendros.js'},default:{priority:-20,resueExistingChunk:true,filename:common.js}
}

决定分割代码放到哪个JS文件里面去,通过test进行索引,然后filename进行命名。

这个缓存组的概念其实就是当你引入不同库,例如jquery和lodash时候,他们会根据规则选择打包到哪一个JS文件里面,形成一组

priority:代码优先级,如果某一个模块执行test后同时符合多个组,那么谁的优先级高,就放到哪个组里面~

reuseExistingChunk:true

会判断某一个模块是否已经加载到别的组了,如果是那么就会直接复用,不会再次引入!

1.6 Chunk是什么

我们的src源代码打包分成了两个JS文件,这个JS文件就叫做Chunk(数据块),main.js是一个chunk,vendors-lodash.js也是一个chunk

2. 懒加载LazyLoading

懒加载其实就是延时加载,即当对象需要用到的时候再去加载。

其实上述讲解CodeSplitting异步模块加载就是懒加载的一种体现。

这里我们对之前异步加载的逻辑改造一下,加入事件来触发异步加载:

function getComponent(){...
}document.addEventListener('click',() => {getComponet().then(element => {docment.body.appendChild(element);})})

这样的话只有你点击页面之后才会异步加载lodash库。

通过这种语法,我们就可以针对某些文件进行懒加载,lodash库变成需要时候才会加载 — 懒加载(通过imort 异步加载)

懒加载的优点:

  1. 提高页面初始化的加载速度,不需要加载额外JS代码
  2. 前端框架的路由概念也是需要基于代码分割和路由切换代码懒加载来实现

3. 打包分析工具

这里插入一些介绍打包分析工具,有利于后面预加载问题的引入

首先我们可以借助一些工具来对webpack打包生成的文件进行分析,看看打包是否合理:

这里可以借助webpack官方的analyse工具,此外在打包时候需要额外参数来生成描述文件:

webpack --profile  --json >stats.json

接着我们借助官方工具来分析网络内容,这个网站需要科学上网(webpack analyse):

image

里面可以看到包括module的引用关系,静态资源,大小,chunks等等信息。

还有一个webpack-chart工具可以以图标可视化形式来分析打包文件大小和组成

image

最后还有webpack-bundle-analyzer这个工具推荐,专门用于分析各个bundle

4. 预加载

4.1 问题引入

其实webpack的目的并不是基于CodeSplitting的优化来让网页仅在第二次加载时才提高网页加载速度,webpack与我们实际上更希望第一次用户加载网页时也可以提高渲染响应速度,这里仅仅使用split分成多个chunk其实就没用了,满足不了需求。

例如如下这段代码:

document.addEventListener('click',() => {const element = document.cteateElement('div');element.innerHTML = 'hello';document.body.appendChild(element)
})

上述代码可以执行,但是借助chrome的devTool-Coverage工具可以分析加载的JS文件的利用率,上述main.js的利用率为74%。

看到有很多部分是红的,这意味这这部分代码还没有执行过,这就是一种浪费,我们希望首次加载的文件尽量小。这里就可以结合CodeSplitting和懒加载,把点击事件内部的处理逻辑放到一个异步加载的模块中进行,我们新增一个click.js:

function handleClick(){const element = document.cteateElement('div');element.innerHTML = 'hello';document.body.appendChild(element)
}export default handleClick;

然后修改index.js,异步调用模块:

document.addEventListener('click',() => {import('./click.js').then(({default:func}) => {func();  })
})

然后重新运行,可以发现代码利用率变高了,这是因为addEventListener的代码变少了,移动到异步模块里面去了,所以利用率高了,这么写代码才是让页面加载速度变快的方式。

其实目前高性能JS的核心不是缓存,而是代码的使用率,可以利用Coverage工具进行分析和优化。

但是上述一系列优化其实还有一个问题,那就是如果懒加载的JS文件较大,那么很可能点击时的交互会很慢(等待JS加载完毕),这里就需要引入prefetching和preloading – 预加载了

4.2 Preloading与Prefetching解决问题

线上网页真正需要立刻加载的是首屏资源,加载完成之后其实带宽就空闲了,并且此时核心逻辑和界面也展示出来了。

那么可不可以此时把上述异步处理的代码下载下来,而不是点击时才下载。这样不就是可以利用好空闲时间嘛!

这里其实webpack的该项优化只需要一个魔法注释就可以了:

document.addEventListener('click',() => {import(/*webpackPrefetch:true*/'./click.js').then(({default:func}) => {func();  })
})

此时一旦发现核心JS文件都加载完成以后,会偷偷加载click.js文件!

打包后测试发现在网络空闲时候会加载1.js文件,同时点击的时候也会加载,但是此时是直接利用缓存,所以速度很快~

这里你也可以改成Preload,区别主要在于:Prefetching 是在核心代码加载完成之后带宽空闲的时候再去加载,而 Preloading 是和核心代码文件一起去加载的。

因此,使用 Prefetching 的方式去加载异步文件更合适一些,不过要注意浏览器的兼容性问题。