前端性能优化二:现代浏览器javascript性能优化(1)

2,860 阅读10分钟

前端性能优化一:性能指标

现代前端程序中,前端资源文件中占绝大部分byte的是javascript。一个现代前端应用程序,javascript包中可能包含以下几种类型的js文件

  • 一个客户端的框架(react/vue等等)或者一个UI框架(Element UI等等)
  • 单页应用程序state管理解决方案(Redux,vuex等等)
  • Polyfill
  • 一些工具库(lodash/Axios等等)

你发送到浏览器的js文件越大,你的加载时间就会越慢

100kb的js文件和100kb的图片文件所消耗的性能是非常不同的

他们可能用了相同的时间下载,但是处理这两种文件的过程会带来非常不同的性能消耗

一个jpeg图片文件下载以后需要被解码,栅格图像,然后渲染到屏幕上。而一个js文件可能花费了相同的时间下载了一个相同大小的js文件,接下来交由js引擎处理我们的js。

js引擎处理管道流程

从拿到你写的js文件开始。js引擎解析(parse)你的源代码将他转换成抽象语法树(AST)。基于这个AST,解析器开始生成字节码(bytecode)。ok到这里引擎已经可以开始执行你的代码了。

但是当你的其中一块代码被执行的次数越来越多,js引擎会将这一块代码标记成热代码(hot),为了让这一块代码执行的更快,之前生成的字节码(bytecode)会传入优化器,优化器会根据之前运行的代码情况,做一些假设(比如根据之前的运行情况猜测变量的类型都是Number类型),然后根据这些假设进行优化。如果因为之前优化时候做出的假设错误,性能优化器会将代码去优化,重新执行之前未优化的bytecode。

这个是一个大致的流程每个浏览器的实现方式都类似但是会有一些区别,在V8中解析器叫做(Ignition),优化器叫做(TurboFan),Ignition生成完bytecode之后,在执行过程中收集执行数据(profiling data),用于在代码变hot之后,交由TurboFan,根据执行数据做出假设进行优化.

SpiderMonkey用于Firefox的内核执行方式有一些不同,他有两个优化器,一个Baseline优化器生成轻度的优化代码,IonMonkey优化器生成高度优化的代码.如果优化器做的假设失败,去优化为Baseline轻度优化代码。 Chakra用于Edge的内核他也是两个优化器,分别为SimpleJIT生成轻度优化代码,FullJIT生成高度优化代码。JavaScriptCore用于Safari和ReactNative的内核有三个优化器。

为什么每个引擎都有不同个数的优化器?

解析器可以快速的生成字节码(bytecode),但是bytecode的执行效率却不是很高,另外一方面优化器可以生成用于高效执行的机器码,但是生成优化代码所需要的时间也会高一些。这是一个快速得到执行代码(bytecode)和得到能够快速执行的代码(机器码)的一个取舍,另外机器码所占用的内存也会高一些。所以优化器在选择是否要优化一块代码的时候是非常复杂的意见事情,举个例子

fun()

假设我们的前端程序只有这一行代码,那么可想而知,我们快速的得到可以执行的bytecode比得到高度优化的机器码所花费的时间要有意义的多。

for(let i = 0;i< 100000;i++){
    fun()
}

同样的一个方法被放在循环里面被重复执行10万次,那么虽然花费了多一点的时间得到机器码,但是因为要执行很多次,所以得到高度优化的机器码所花费的时间也是划算的。

因为之前提到过高度优化的代码所占用的内存也会高一些,那么考虑是否优化某块代码的时候还要考虑运行终端的内存情况,比如在内存非常小的手机上,优化器选择是否优化某块代码就要谨慎的多。

那么如果跑在服务器端Node.js的环境下,因为程序可能只启动一次,但是需要运行多次,那么第一次启动的时间对程序来说意义不是很大,相反的能够快速执行的机器码就要有意义的多。

优化器需要根据当前终端的内存情况和代码使用情况决定是否优化代码

所以一些引擎选择多个优化器,可以更好的控制这个复杂度。

但是总的来说,这些引擎执行js代码的顺序是一样的,架构也是一样的(SoureCode->AST->Bytecode->MachineCode).

说了这么多就是想说js是现在浏览器中最消耗性能的资源,不同设备和网络环境所带来的性能表现差异也比较大,终端性能越差,当然花的时间对应的也越多,所以发送到浏览器的js文件越小越好。

tl;dr:

只加载当前首屏需要的js文件

利用code split将文件拆分,给他们排出优先级,只加载首屏用户需要的js文件,将其他的js文件懒加载,能够较大程度的减少js文件的大小,能够显著的加快loading时间和TTI的时间。

学会分析你的js包,把你不需要的js弄出去

开发者如果没有这个意识,有很大的可能性将一整个工具库打包到一个bundles里面,但是其实你只用了其中一个方法。

tree sharking现在应该不是什么新鲜的单词了,最早由Rollup提出,后来因为webpack的影响力,发扬光大。

code split和tree sharking这里就不展开介绍了,网上已经有很多好的文章。

如果你已经服务器渲染HTML,考虑一下其实你真的需要客服端的这个库吗?

这一条主要针对vue或者react的universal程序,也就是说你的程序同时在服务器端和客户端依赖了vue或者react框架,先利用服务器端渲染进行html渲染,然后在转为前端路由。这的确很酷,但是你有没有想过,当你已经在服务器端利用vue或者react的服务器端渲染将html生成了,在一些简单的页面其实你是不需要将vue或者react发送到客户端的。Netfix:是一个美国的视频网站,他们依赖了react,他们的开发者尝试在注册页去掉了前端的react,只在服务器端渲染的时候依赖react渲染html,也就是说将react也按需加载了,将注册页的js大小减少了70%,大大的提高了性能。

利用tree sharking和code split将你的js文件减少了以后,我们始终还是有js文件的。那我们还能做什么呢?

选择合适的方式引入js文件(preload,async)

当浏览器获得服务器发送的HTML标签,开始从上到下进行解析。碰到js标签,就会发送js请求,然后进行解析,编译,执行。在进行这一系列动作的时候主线程都是堵塞的。但是现在我们有了更多的选择来引入一个js文件。

<link rel="preload" href="one.js" as="script">

新版本的浏览器会在接收到HTML以后,先快速的检查一遍所有的html标签,如果碰到了preload,他会直接去请求这个文件,然后根据as后面的文件类型,对文件进行处理,如果是js就会解析,编译。那么什么时候preload的js文件会执行呢?

<script src="one.js" ></script>

当你碰到正常的一个script标签或者你可以onload的时候直接让他执行

<link rel="preload" as="script" href="one.js"
onload="var script = document.createElement('script');
        script.src = this.href;
        document.body.appendChild(script);">

不论哪种方式,浏览器都能更早的请求js文件,然后执行的时候不需要等待原本都是堵塞主线程的下载,解析,编译。 preload不止可以提前loadjs还可以用于其他类型的文件,可以获得全部的list

<script src="one.js" async></script>

如果你需要引入一些第三方的js文件,比如baidu统计之类的第三方库。你可以让这个标签打上async属性,当浏览器看到这个属性的时候,这个js就不会堵塞线程。并且在新版本的chrome浏览器里,async标签他的解析,编译都会在子线程里进行,这个对性能是很划算的。

尽量多的利用浏览器平行可以请求标签的数量,聪明的打包你的js,合理的利用缓存

不同的浏览器不同版本能够同时发起平行请求的数量不同,如果http2的标准那么能同时发送的请求就更多了。具体的数量我们这里就不讨论了。能同时发送的请求越多,意味着我们可以尽可能多的在一个页面不同组件中,将可以共用的代码的打包到一起,也不用花两个阶段去请求js。在打包的时候,尽可能的把不怎么改变的文件和可能会发生改变的文件分开来打包,这样就可以充分利用缓存。对于特别大的js打包文件,尽量可以在同时请求数允许的范围里将文件打包成大于等于50kb一个。

为什么是50kb?

新版本的chrome已经开始支持js文件stream解析,也就是说当你的js文件还没完全下载完毕的时候,js引擎就可以分块的将js stream放在工作子线程进行解析。但是单个文件必须大于50kb才会使用stream parse,所以将一个大的js包,分开打包为多个50kb的js包,平行请求,chrome会使用stream parse,并且将解析,编译的工作放到子线程进行,这样主线程就可以响应交互请求或者解析HTML。 例如facebook,通过大约292个请求加载了大约6M的压缩过的js包,他利用async和stream parse将子线程的利用率大大的提高。

facebook利用async和stream parse将子线程利用率大大提高

NOTE:inline Script和缓存中加载的js文件没办法使用stream parse。

长任务

js长任务在之前已经介绍过,一直堵塞主线程,导致主线程无法处理用户响应和其他任务的超过50ms的任务,我们认为是长任务。

那么怎么可以缩短长任务呢?基本的思路就是把任务切片,设置优先级将长任务分块的去执行。 如果你使用的是vue或者react这种响应式的框架。多个组件同时需要渲染,会引发多个组件相继出发init->render->patch,很有可能出现一个超过50ms的长任务。那你要分析有些组件是不是可以放到后面去渲染(比如不是首屏需要的组件),或者两个组件分开时间去渲染。

结论

在现代浏览器中js文件仍然是加载时最消耗性能的文件。所以利用code split和tree sharking让js文件尽可能的小,只加载首屏需要的文件是有效提高性能的办法。另外使用preload和async来获得js。学会分析js包,利用缓存和stream parse也可以有效的提高性能。下一章我们讨论js另外的一些提高性能的办法。