前端性能优化:细说JavaScript的加载与执行

本文主要是从性能优化的角度来探讨JavaScript在加载与执行过程当中的优化思路与实践方法,既是细说,文中在涉及原理性的地方,难免会多说几句,还望各位读者保持耐心,仔细理解,请相信,您的耐心付出必定会让您获得与之匹配的回报。javascript

缘起

随着用户体验的日益重视,前端性能对用户体验的影响备受关注,但因为引发性能问题的缘由相对复杂,咱们很难但从某一方面或某几个方面来全面解决它,这也是我行此文的缘由,想以此文为起点,用一系列文章来深层次探讨与梳理有关Javascript性能的方方面面,以填补并夯实本身的知识结构。css

目录结构

本文大体的行文思路,包含但不局限:html

  • 不得不说的JavaScript阻塞特性前端

  • 合理放置脚本位置,以优化加载体验,js脚本放在 <body>标签闭合以前。java

  • 减小HTTP请求次数,压缩精简脚本代码。node

  • 无阻塞加载JavaScript脚本:web

    • 使用<script>标签的defer属性。后端

    • 使用HTML5的async属性。跨域

    • 动态建立<script>元素加载JavaScript。浏览器

    • 使用XHR对象加载JavaScript。

不得不说的JavaScript的阻塞特性

前端开发者应该都知道,JavaScript是单线程运行的,也就是说,在JavaScript运行一段代码块的时候,页面中其余的事情(UI更新或者别的脚本加载执行等)在同一时间段内是被挂起的状态,不能被同时处理的,因此在执行一段js脚本的时候,这段代码会影响其余的操做。这是JavaScript自己的特性,咱们没法改变。

咱们把JavaScript的这一特性叫作阻塞特性,正由于这个阻塞特性,让前端的性能优化尤为是在对JavaScript的性能优化上变得相对复杂。

为何要阻塞?

也许你还会问,既然JavaScript的阻塞特性会产生这么多的问题,为何JavaScript语言不能像Java等语言同样,采用多线程,不就OK了么?

要完全理解JavaScript的单线程设计,其实并不难,简单总结就是:最初设计JavaScript的目的只是用来在浏览器端改善网页的用户体验,去处理一些页面中相似表单验证的简单任务。因此,那个时候JavaScript所作的事情不多,而且代码不会太多,这也奠基了JavaScript和界面操做的强关联性。

既然JavaScript和界面操做强相关,咱们不妨这样理解:试想,若是在某个页面中有两段js脚本都会去更改某一个dom元素的内容,若是JavaScript采用了多线程的处理方式,那么最终页面元素显示的内容究竟是哪一段js脚本操做的结果就不肯定了,由于两段js是经过不一样线程加载的,咱们没法预估谁先处理完,这是咱们不想要的结果,而这种界面数据更新的操做在JavaScript中比比皆是。所以,咱们就不难理解JavaScript单线程的设计缘由:JavaScript采用单线程,是为了不在执行过程当中页面内容被不可预知的重复修改

关于JavaScript的更多“身世”之谜,能够看阮一峰老师的Javascript诞生记

从加载上优化:合理放置脚本位置

因为JavaScript的阻塞特性,在每个<script>出现的时候,不管是内嵌仍是外链的方式,它都会让页面等待脚本的加载解析和执行,而且<script>标签能够放在页面的<head>或者<body>中,所以,若是咱们页面中的css和js的引用顺序或者位置不同,即便是一样的代码,加载体验都是不同的。举个栗子:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>js引用的位置性能优化</title>
    <script type="text/javascript" src="index-1.js"></script>
    <script type="text/javascript" src="index-2.js"></script>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
复制代码

以上代码是一个简单的html界面,其中加载了两个js脚本文件和一个css样式文件,因为js的阻塞问题,当加载到index-1.js的时候,其后面的内容将会被挂起等待,直到index-1.js加载、执行完毕,才会执行第二个脚本文件index-2.js,这个时候页面又将被挂起等待脚本的加载和执行完成,一次类推,这样用户打开该界面的时候,界面内容会明显被延迟,咱们就会看到一个空白的页面闪过,这种体验是明显很差的,所以咱们应该尽可能的让内容和样式先展现出来,将js文件放在<body>最后,以此来优化用户体验

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>js引用的位置性能优化</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div id="app"></div>
    <script type="text/javascript" src="index-1.js"></script>
    <script type="text/javascript" src="index-2.js"></script>
  </body>
</html>
复制代码

从请求次数上优化: 减小请求次数

有一点咱们须要知道:页面加载的过程当中,最耗时间的不是js自己的加载和执行,相比之下,每一次去后端获取资源,客户端与后台创建连接才是最耗时的,也就是大名鼎鼎的Http三次握手,固然,http请求不是咱们这一次讨论的主题,想深刻了解的自行搜索,网络上相关文章不少。

所以,减小HTTP请求,是咱们着重优化的一项,事实上,在页面中js脚本文件加载很不少状况下,它的优化效果是很显著的。要减小HTTP的请求,就不得不提起文件的精简压缩了。

文件的精简与压缩

要减小访问请求,则必然会用到js的**精简(minifucation)和压缩(compression)**了,须要注意的是,精简文件实际并不复杂,但不适当的使用也会致使错误或者代码无效的问题,所以在实际的使用中,最好在压缩以前对js进行语法解析,帮咱们避免没必要要的问题(例如文件中包含中文等unicode转码问题)。

解析型的压缩工具经常使用有三:YUI Compressor、Closure Complier、UglifyJs

YUI Compressor: YUI Compressor的出现曾被认为是最受欢迎的基于解析器的压缩工具,它将去去除代码中的注释和额外的空格而且会用单个或者两个字符去代替局部变量以节省更多的字节。但默认会关闭对可能致使错误的替换,例如with或者eval();

Closure Complier: Closure Complier一样是一个基于解析器的压缩工具,他会试图去让你的代码变得尽量小。它会去除注释和额外的空格并进行变量替换,并且会分析你的代码进行相应的优化,好比他会删除你定义了但未使用的变量,也会把只使用了一次的变量变成内联函数。

UglifyJs:UglifyJs被认为第一个基于node.js的压缩工具,它会去除注释和额外的空格,替换变量名,合并var表达式,也会进行一些其余方式的优化

每种工具都有本身的优点,好比说YUI压缩后的代码准确无误,Closure压缩的代码会更小,而UglifyJs不依靠于Java而是基于JavaScript,相比Closure错误更少,具体用哪一个更好我以为没有个确切的答案,开发者应该根据本身项目实际状况酌情选择。

从加载方式上优化:无阻塞脚本加载

在JavaScript性能优化上,减小脚本文件大小并限制HTTP请求的次数仅仅是让界面响应迅速的第一步,如今的web应用功能丰富,js脚本愈来愈多,光靠精简源码大小和减小次数不老是可行的,即便是一次HTTP请求,但文件过于庞大,界面也会被锁死很长一段时间,这明显很差的,所以,无阻塞加载技术应运而生。

简单来讲,就是页面在加载完成后才加载js代码,也就是在window对象的load事件触发后才去下载脚本。 要实现这种方式,经常使用如下几种方式:

延迟脚本加载(defer)

HTML4之后为<script>标签订义了一个扩展属性:defer。defer属性的做用是指明要加载的这段脚本不会修改DOM,所以代码是能够安全的去延迟执行的,而且如今主流浏览器已经所有对defer支持。

<script type="text/javascript" src="index-1.js" defer></script>
复制代码

带defer属性的<script>标签在DOM完成加载以前都不会去执行,不管是内嵌仍是外链方式。

延迟脚本加载(async)

HTML5规范中也引入了async属性,用于异步加载脚本,其大体做用和defer是同样的,都是采用的并行下载,下载过程当中不会有阻塞,但不一样点在于他们的执行时机,async须要加载完成后就会自动执行代码,可是defer须要等待页面加载完成后才会执行

从加载方式上优化:动态添加脚本元素

把代码以动态的方式添加的好处是:不管这段脚本是在什么时候启动下载,它的下载和执行过程都不会则色页面的其余进程,咱们甚至能够直接添加带头部head标签中,都不会影响其余部分。

所以,做为开发的你确定见到过诸如此类的代码块:

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'file.js';
document.getElementsByTagName('head')[0].appendChild(script);
复制代码

这种方式即是动态建立脚本的方式,也就是咱们如今所说的动态脚本建立。经过这种方式下载文件后,代码就会自动执行。可是在现代浏览器中,这段脚本会等待全部动态节点加载完成后再执行。这种状况下,为了确保当前代码中包含的别的代码的接口或者方法可以被成功调用,就必须在别的代码加载前完成这段代码的准备。解决的具体操做思路是:

现代浏览器会在script标签内容下载完成后接收一个load事件,咱们就能够在load事件后再去执行咱们想要执行的代码加载和运行,在IE中,它会接收loaded和complete事件,理论上是loaded完成后才会有completed,但实践告诉咱们他两彷佛并无个前后,甚至有时候只会拿到其中的一个事件,咱们能够单独的封装一个专门的函数来体现这个功能的实践性,所以一个统一的写法是:

function LoadScript(url, callback) {
        var script = document.createElement('script');
        script.type = 'text/javascript';

        // IE浏览器下
        if (script.readyState) {
          script.onreadystatechange = function () {
            if (script.readyState == 'loaded' || script.readyState == 'complete') {
              // 确保执行两次
              script.onreadystatechange = null;
              // todo 执行要执行的代码
              callback()
            }
          }
        } else {
          script.onload = function () {
            callback();
          }
        }

        script.src = 'file.js';
        document.getElementsByTagName('head')[0].appendChild(script);
      }

复制代码

LoadScript函数接收两个参数,分别是要加载的脚本路径和加载成功后须要执行的回调函数,LoadScript函数自己具备特征检测功能,根据检测结果(IE和其余浏览器),来决定脚本处理过程当中监听哪个事件。

实际上这里的LoadScript()函数,就是咱们所说的LazyLoad.js(懒加载)的原型。

有了这个方法,咱们能够实现一个简单的多文件按某一固定顺序加载代码块:

LoadScript('file-1.js', function(){
  LoadScript('file-2.js', function(){
    LoadScript('file-3.js', function(){
        console.log('loaded all')
    })
  })
})
复制代码

以上代码执行的时候,将会首先加载file-1.js,加载完成后再去加载file-2.js,以此类推。固然这种写法确定是有待商榷的(多重回调嵌套写法简直就是地狱),但这种动态脚本添加的思想,和加载过程当中须要注意的和避免的问题,都在LoadScript函数中得以澄清解决。

固然,若是文件过多,而且加载的顺序有要求,最好的解决方法仍是建议按照正确的顺序合并一块儿加载,这从各方面讲都是更好的法子。

从加载方式上优化:XMLHttpRequest脚本注入

经过XMLHttpRequest对象来获取脚本并注入到页面也是实现无阻塞加载的另外一种方式,这个我以为不难理解,这其实和动态添加脚本的方式是同样的思想,来看具体代码:

var xhr = new XMLHttpRequest();
xhr.open('get', 'file-1.js', true);
xhr.onreadystatechange = function() {
  if(xhr.readyState === 4) {
    if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304){
      // 若是从后台或者缓存中拿到数据,则添加到script中并加载执行。
      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.text = xhr.responseText;
      // 将建立的script添加到文档页面
      document.body.appendChild(script);
    }
  }
}

复制代码

经过这种方式拿到的数据有两个优势:其一,咱们能够控制脚本是否要当即执行,由于咱们知道新建立的script标签只要添加到文档界面中它就会当即执行,所以,在添加到文档界面以前,也就是在appendChild()以前,咱们能够根据本身实际的业务逻辑去实现需求,到了想要让它执行的时候,再appendChild()便可。其二:它的兼容性很好,全部主流浏览器都支持,它不须要想动态添加脚本的方式那样,咱们本身去写特性检测代码;

但因为是使用了XHR对象,因此不足之处是获取这种资源有“域”的限制。资源 必须在同一个域下才能够,不能够跨域操做。

最后总结

文章主要从JavaScript的加载和执行这一过程当中挖掘探讨对前端优化的解决方案,并较细致的罗列了各个解决方案的优点和不足之处,固然,前端性能优化本就相对复杂,要想完全理解其各中起因,还有很长一段路要走!

本文主要行文思路:

  • 不得不说的JavaScript阻塞特性

  • 合理放置脚本位置,以优化加载体验,js脚本放在 <body>标签闭合以前。

  • 减小HTTP请求,压缩精简脚本代码。

  • 无阻塞加载JavaScript脚本:

    • 使用<script>标签的defer属性。

    • 使用HTML5的async属性。

    • 动态建立<script>元素加载JavaScript。

    • 使用XHR对象加载JavaScript。

最后,因为我的水平缘由,如有行文不全或疏漏错误之处,恳请各位读者批评指正,一路有你,不胜感激!。

感谢这个时代,让咱们能够站在巨人的肩膀上,窥探程序世界的宏伟壮观,我愿以一颗赤子心,踏遍程序世界的千山万水!愿每个行走在程序世界的同仁,都活成心中想要的样子,加油!

相关文章
相关标签/搜索