前端原生JS代码按需加载
问题描述
最近公司需要对产品的性能进行提升,但是,因为产品的结构是原生es5代码写出来的,这就存在一下几个问题
1.首页加载缓慢
因为是单页面的app,并且产品的结构比较庞大,一个页面引入了300多个js文件以及10几个css文件,这就导致了产品的首页加载非常缓慢,具体是有多慢呢?大概是7s左右。这肯定是不符合现代的这种前端的趋势的。非常影响体验。
2.请求并发数多
文件的加载量太过于庞大,对于高并发首页面加载的情况下,后台服务的确面临着很大的压力。
解决思路
首先,对于第一个问题,我们的产品是一个单页面,未使用框架开发的产品,所以,按需加载的模式无法使用现在前端非常流行的框架进行改造,如果这样的话,投入的人力会较大,不如直接开发一个新的产品。所以,针对这种情况,能想到的是通过其他的思路,进行优化。
在前端发展至框架化之前,按需加载似乎能想到的是requireJs等那一系列的按需架子框架。,但是问题是,需要原生的js代码支持这种加载方式,还需要重新梳理各个文件及各个类之间的关系,这样也是一个很费劲的事情。这种想法也是被排除掉了
所以问题就来了,但是还有好的一方面是,我们的产品是面向对象的方式进行各个模块之间的逻辑连接的。所以,我想到的是,只需要在创建类的对象时,做一些文章。
1.单个类的按需加载
加入现在有一个类,名称是classA, 在产品中的文件名是a.js,
function classA (){
}
classA.prototype.a = function(){
}
我们在new classA时,如果没有classA,当然会报错,但是如果我们重现改写一下现在classA的构造函数
function classA (){
var xhr = new XMLHttpRequest;
xhr.open('GET', './a.js', false)
if(xhr.readyState === 4 && xhr.status === 200) {
var script = document.createElement('script');
script.text = "//@sourceURL=" + './a.js' + '\n' + xhr.response;
document.head.appendChild(script)
}
return new classA
}
这样,就可以保证我需要加载classA的时候,如过classA是我们需要的classA,会走构造函数1,但是如果不是的话,首先会重新请求‘./a.js’,然后classA构造函数会被重构,然后重新创建classA的对象,返回即可。这样就保证了classA的按需加载。
但是,有问题的是,在项目具体实践中,有些类是用es6语法写的。例如现在,真实的classA使用这种方式声明
class classA{
}
你会发现,继续使用这种方法,是会报错的;这是因为在es6语法中,不允许class声明类时,类的名称重复。
所以,我们需要对classA做如下改造
class classA_1{
}
var classA = classA_1
这样,就不会发生类名重复的错误了。就可以用之前的方案进行项目改造。
2.类的继承
上述思路解决了单个类的按需加载,然而问题是大多数情况下,类之间是有继承关系的。
那么我们看一下es5的类继承如何进行的,这里就不说具体的逻辑,只列出代码
function extend(Sub, Super) {
Sub.prototype = new Super();
//改成:
Sub.prototype = Object.create(Super.prototype);
}
我们可以看到,子类Sub在继承基类Super时,会先创建基类的对象,这其实,就和我们之前的思路,保持一致。只需要重写基类的构造函数,即可。
然而对于es6,继承的逻辑是
class classSub extends classSuper{
}
此时,继承的逻辑我们不能自定义,所以,这就需要子类在继承基类之前,保证基类的构造函数存在并且是我们所要的。
所以,我们需要这样重构classSub的构造函数
function classSub (){
new classSuper
var xhr = new XMLHttpRequest;
xhr.open('GET', './subclass.js', false)
if(xhr.readyState === 4 && xhr.status === 200) {
var script = document.createElement('script');
script.text = "//@sourceURL=" + './subclass.js' + '\n' + xhr.response;
document.head.appendChild(script)
}
return new classSub
}
这样,就可以保证classSuper是在classSub构造函数重写时存在并且是我们所要的构造函数。
3.解决切换时卡顿问题
根据以上的方案,的确解决了按需加载js文件的问题,我们的产品,首页面加载速度也从之前的7s左右优化到了800ms左右,之前的300多个请求,也被降到了20多个,这也达成了预想。但是,这也造成了问题,在切换到具体的应用组件页面时,因为会多出很多同步的请求,这会造成卡顿,比如,我们产品实际遇到过,一个页面应用比较复杂,这需要加载170多个js文件,还是同步的请求,这就造成了3-4s多之后,具体的页面才出现,这不是我们所希望看到的。
所以,我们需要进一步对js文件的加载进行改造。最先想到的是,利用闲置时间,加载剩余js文件。按照这个思路,我最先想到的是 webWorker,webWorker是能够开启浏览器多线程的一个法宝,虽然有很多限制,但是用于发送请求,加载js文件的话,确实是一个不错的选择。
所以,我这里建立一个workerload.js的文件,里面写的逻辑就是加载读取js文件的数据,并且将数据返给主线程,这里值得注意的是,需要返回给主线程一个具体的类名,用来判断主线程是否加载过这个类。所以,最后的成品是这样的,在主线程中,我们需要重现定义各个类的构造函数。如
function classLoading(url){
var xhr = new XMLHttpRequest;
xhr.open('GET', url, false)
if(xhr.readyState === 4 && xhr.status === 200) {
var script = document.createElement('script');
script.text = "//@sourceURL=" + url + '\n' + xhr.response;
document.head.appendChild(script)
}
}
function classSub (...data){
new classSuper
var url = './classsub.js'
classLoading(url)
classSub.alreadyLoaded = true; //表示该类已加载
return new classSub(...data)
}
其中用到class的静态属性,将这个类标记为已加载的类,防止重复加载。
之后,建立workerload.js,在子线程中利用空闲时间加载js文件;
function classLoading(file){
var xhr = new XMLHttpRequest;
xhr.open('GET', file.url, true);
xhr.send(null)
if(xhr.readyState === 4 && xhr.status === 200) {
var result = {
name:file.name,
content:xhr.response,
url:file.url
}
postMessage(result)
}
}
var files = [
{name:'classSub',url:'./classsub.js'}, ...
]
files.forEach(file=>{
classLoading(file)
})
可以看到的是,在webworker加载js时,可以使用异步方案加载js文件,这样会大大提高加载js文件的性能。但是有问题的是,如果加载的js文件有前后依赖关系,需要先加载优先级较高的,这需要用到同步加载。我在项目改造的时候就是用的这种方案。
有了workerload.js之后,就可以在主线程中进行空闲时间加载js文件了
window.loadFIleWorker = new Worker('./workerload.js')
loadFIleWorker.onmessage = event =>{
if(window[event.data.name] && !window[event.data.name].alreadyLoaded) {
window[event.data.name].alreadyLoaded = true; //用来标记该类已加载
var script = document.createElement('script');
script.text = "//@sourceURL=" + event.data.url + '\n' + event.data.content;
document.head.appendChild(script)
}
}
这里具体就是使用webworker开启多线程,在worker线程中用来加载js,并且监听worker线程发送过来的数据,用来动态的将js脚本写入到页面中
值得注意的是,我在项目中遇到的问题是,一次性用worker线程加载大量的js,导致主线程会持续接收worker线程发来的数据,这就会造成一段时间的卡顿,为了解决这个问题,我令worker线程分批次加载js文件,一次40个左右,可以挑选合适的时机去加载js文件
4.文件的合并
为了解决请求并发数多的问题,我是针对文件进行合并,减少并发量,这样可以解决服务器并发带来的压力。这里可以根据自己的具体项目进行
总结
本文是解决使用无框架化开发的项目按需加载的问题,可以解决首页面加载慢的问题,以及各个应用切换缓慢的问题。
主要的方案总结如下
- 重写构造函数,(如果继承方案和上述不一致,没有new基类,也需要改造)
- 利用webWorker进行空闲时间的js文件加载。
- 文件合并,减少请求数