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>