Tiny-loader 好用的资源加载器

Tiny-Loader 项目已经在github上开源,具体可看: Tiny-Loader

概述

在前端性能优化中,我们会压缩静态文件,懒加载图片,合并请求,来加快页面打开速度。当这些都做完以后,前端性能优化仿佛进入了一个瓶颈,所有的资源都已经最合理化加载了。其实,仔细观察静态资源文件,会发现许多文件我们并不需要在页面一开始就下载它们。这时候,如果有个组件,帮助我根据优先级的不同,在特定的时间下载特定的资源,同时需要保证脚本的执行顺序,就能完美的解决这个问题。这个就是 Tiny-Loader 的起源。它与一般资源加载器不同的是,它可以保证资源下载以后的执行顺序,可以按需进行资源加载。

关于浏览器渲染机制,特别是js对首屏时间的影响,可以移步我的小伙伴 @德来 在SegmentFault上的专栏文章《JS 一定要放在 Body 的最底部么?聊聊浏览器的渲染机制》

为什么使用 Tiny-Loader

在上古时代,我们讲究的是把所有css都放在页面头部,把所有js放在页面底部。这样可以让页面迅速展示出来,而js的阻塞执行不会影响到body内部元素的渲染。其实,大部分的js加载可以放到load事件后再加载,这样可以释放出许多网络资源,让页面更快的展现在用户面前。

同时,在前端性能优化过程中,发现许多js,css并不是页面一开始就需要的,而是在用户某个操作以后,才需要执行/渲染出来的。将那些js、css缓加载,可以大大减小页面的首屏时间,减少页面出load事件的负担。

使用方法

Loader.async: 下载远端js,css文件,异步执行,适用于对执行顺序不重要的js文件

Loader.async(['xxxx.css', 'yyyy.js'])  

Loader.sync: 下载远端js,css文件,同步执行,适用于对执行顺序有要求的js文件

Loader.sync(['xxxx.css', 'yyyy.js', 'zzzz.js'])  

Loader callback: Tiny-Loader 支持回调函数,在资源下载完以后,会执行回调函数

Loader.sync(['xxxx.css', 'yyyy.js', 'zzzz.js'], function(){  
 console.log('finish downloading');
})

注意:

  • 普通的js,可以委托给Tiny-Loader进行下载。Tiny-Loader会在页面load以后才开始js的加载,让其余资源先行加载,保证Dom树更早的生成完毕。
  • 对于js的执行顺序问题,Tiny-Loader里面的Loader.sync能够保证js下载后的执行顺序,这样就能保证js同时下载,顺序执行。
  • css加载都默认走async方式
  • 会自动判断文件类型,用正确的方式加载文件
<html>  
  <head></head>
  <body>
    <div class="container">container</div>
    <script>
      Loader.sync(['xxxx.js', 'yyyy.js', 'zzzz.js'])
    </script>
  </body>
</html>  

进阶使用

接下来我们根据概述里讲的,研究下 Tiny-Loader 的高级玩法。

使用模块

在实际开发中,我们会以模块的方式来写html,对应的模块都有自己所需要的js。传统的做法是把所有模块js压缩在一起,在页面底部统一引用。但是这样做的坏处就是模块html和js是分离开的,新建一个模块,就需要在js配置文件里面加一个新的文件引入。而且,更新一个模块,就会导致用户重新下载所有模块打包在一起的js。

解决方法就是把各自模块需要的js文件放在对应的html里,然后通过Tiny-loader引用,就可以保证每个模块文件里有各自的html和js,模块间互不影响,js也会在页面最后才会下载,不会堵塞住页面的渲染

<html>  
  <head></head>
  <body>
    <header>
      <h1>What Does WWF Do?</h1>
      <p>WWF's mission:</p>
    </header>

    <main>
      <div id="components1"></div>
      <script>
        Loader.sync(['zepto.js', 'common.js'])
      </script>

      <script>
        Loader.sync(['components1.js'])
      </script>

      <div id="components2">
      ...
      </div>
      <script>
        Loader.sync(['components2.js'])
      </script>

    </main>

    <footer></footer>

    <script>
      Loader.sync(['xxxx.js', 'yyyy.js', 'zzzz.js'])
    </script>
  </body>
</html>  

统一管理页面script请求

上面已经解决了模块化的问题,但是每次js引用都需要写script,而且最终页面上会出现大量的script标签。对于async加载的js,我们都希望它在浏览器下载队列上排在后面。直接调用Loader可能会出现async在sync前下载,占据一个浏览器的并发请求。

<html>  
  <head></head>
  <body>
    <header>
      <h1>What Does WWF Do?</h1>
      <p>WWF's mission:</p>
    </header>

    <main>
      <div id="components1"></div>
      <script>
        Loader.sync(['zepto.js', 'common.js'])
      </script>

      <script>
        Loader.sync(['components1.js'])
      </script>

      <div id="components2">
      ...
      </div>
      <script>
        Loader.sync(['components2.js'])
      </script>

    </main>

    <footer></footer>

    <script>
      // 此处的async调用就会导致 `statistic.js` 比下面的三个js先开始下载
      Loader.async(['statistic.js'])
    </script>

    <script>
      Loader.sync(['xxxx.js', 'yyyy.js', 'zzzz.js'])
    </script>
  </body>
</html>  

但是,配合后端模板渲染引擎,就可以完美的解决这个问题。比如我们使用php,在php模板渲染引擎里自定义两个函数loadLazyJs,replaceLazyJS。loadLazyJs这个方法用来帮我们管理要加载的js,而不是在调用的地方立刻输出script标签。replaceLazyJS这个方法在html生成以后调用,在页面底部统一输出一段调用Tiny-loader的js代码

调用的效果

<html>  
  <head></head>
  <body>
    <header>
      <h1>What Does WWF Do?</h1>
      <p>WWF's mission:</p>
    </header>

    <main>
      <div id="components1"></div>
      <?php $view->loadLazyJS(['zepto.js', 'common.js']); ?>

      <?php $view->loadLazyJS('components1.js'); ?>

      <div id="components2">
      ...
      </div>
      <?php $view->loadLazyJS('components2.js'); ?>

    </main>

    <footer></footer>

    <?php $view->loadLazyJS('statistic.js', true); ?>

    <?php $view->loadLazyJS(['xxxx.js', 'yyyy.js', 'zzzz.js']); ?>
  </body>
</html>  

最终输出的页面

<html>  
  <head></head>
  <body>
    <header>
      <h1>What Does WWF Do?</h1>
      <p>WWF's mission:</p>
    </header>

    <main>
      <div id="components1"></div>

      <div id="components2">
      ...
      </div>

    </main>

    <footer></footer>

    <script type="text/javascript">
      Loader.sync(['zepto.js', 'common.js', 'components1.js', 'components2.js', 'xxxx.js', 'yyyy.js', 'zzzz.js']);
      Loader.async(['statistic.js']);
    </script>
  </body>
</html>

这样子开发起来就流畅多了,而且实现loadLazyJS,replaceLazyJS也并不困难。实现代码如下:

private $asyncList = array();  
private $syncList  = array();  
/**
* 添加需要延时加载(在onload后)的js
*
* @param {string|Array} $loadJs
* @param {boolean} $async 默认为false
*
*/
public function loadLazyJS($loadJs, $async = false)  
{
  if (empty($loadJs)) {
    return;
  }

  if( is_string($loadJs) ){
    $loadJs = [$loadJs];
  }

  // async js 单独处理
  if( $async ) {
    $this->asyncList = array_merge($this->asyncList, $loadJs);
    return;
  }

  // sync js 需要注意js排列顺序
  $this->syncList = array_merge($this->syncList, $loadJs);
}

/**
* 注入延迟加载的script标签
*/
public function replaceLazyJS($html)  
{
  $scriptStr = '';
  $asyncList = $this->asyncList;
  $syncList  = $this->syncList;

  if (count($syncList) > 0 ) {
    $scriptStr .= 'window.Loader.sync(' . json_encode($syncList) . ');';
  }
  if (count($asyncList) > 0) {
    $scriptStr .= 'window.Loader.async(' .  json_encode($asyncList) . ');';
  }

  if(!$scriptStr) {
    return $html;
  }
  $scriptStr = '<script>'. $scriptStr .'</script>';

  $bodyTagLastPos = strrpos($html, '</body>', -1);
  $html = substr($html, 0, $bodyTagLastPos) . $scriptStr . substr($html, $bodyTagLastPos);

  return $html;
}

资源延后加载

对于许多不是一开始就需要的静态资源,可以延后到合适的时间点再加载。页面上会有一些交互,需要点击按钮以后,渲染某些元素到页面。传统的做法是把所有的css在页面一开始就加载进来。但是,现在有一种更好的做法,即在点击按钮以后再加载对应的css,减少页面最初加载的css大小。包括js也可以这么玩耍。

<html>  
  <head></head>
  <body>
    <button class="js-click">点我</button>
    <script type="text/javascript">
      // 当出现某种交互后,需要下载静态资源 
      var nBtn = document.getElementsByClassName('js-click')[0];
      nBtn.addEventListener('click', function(e){
        window.Loader.async([
          'a.css',
          'b.js',
          'c.js'
        ], function(){
          alert('download finish');
        });
      });
    </script>
  </body>
</html>  
欢迎关注我们的公众号