Preload Javascript
题图来自网络
1 前言
主要介绍几种解决预加载 JS 文件的方案:异步请求 JS 文件,顺序执行 JS 代码。 首先,让我们美好的设想一下:
var script = document.createElement("script") script.noexecute = true script.src = "xxxx.js" document.body.appendChild(script) script.execute()
但这个梦想有一个残酷的现实:
如果你使用 script
引入的 JS 代码,那么,一旦浏览器下载完成,就会立即 执行 。不过留给你任何犹豫的时间。
所以,上面只是一个美好的设想,无法将JS的加载和执行的过程割裂开来(通过 script
标签引入)。
况且,现实中不存在这个 noexecute
和 execute
。
不过,我们可以使用现有的一些技术组合创造出这种效果来。
2 解决
整体解决方案的思路大致分成2个部分:下载和执行。
2.1 下载
2.1.1 script 标签
通过 <script>
标签及其属性 src
引入 JS 文件是常规的做法。无论是“静态”的插入 HTML 中,
还是“动态”的使用 JS 代码插入,根据浏览器的原理1,是没有办法达到我们想要的效果的。
在这里,我们排除这种方式。
2.1.2 Ajax 技术
使用 Ajax 技术加载 JS 文件。通过发送一个单独的请求获取 JS 文本, 亦可做到精细度的下载过程控制,配合定时器,还可以做出下载进度的 UI,简直就是前端开发的美梦啊。 目前浏览器主要提供了以下API:
- XMLHttpRequest
- fetch(Browser compatibility)
这里可以稍微了解一下浏览器并发请求的知识,以及跨域请求(CROS)的限制。 并发请求的技术细节可以参考这个回答:知乎#20474326。 跨域请求的技术细节,我推荐阅读一下阮一峰老师的这两篇文章:浏览器同源政策及其规避方法(阮一峰)和跨域资源共享 CORS 详解(阮一峰)。 掌握这些技术细节有助于你后面更好的代码实现。
2.1.3 jQuery.getScript
最简单方式,使用 jQuery.getScript
函数,可以获取一个 JS 文件。但是这个效果和使用 <script>
标签的效果一致。
2.1.4 Prerender and Prefetch
prefetch
属性是微软针对 IE11 浏览器开发的黑科技,主要用于预先下载部分资源,提升后续操作效率和体验。
具体的技术细节可以看Prerender and prefetch support(MSDN)。因为和本文的关联性不是很大,在此不详细说明。
说到 Prerender and Prefetch 顺带提一下 IE 的 <script>
标签专有属性 defer
和现代浏览器支持的 async
属性(caniuse),
defer
属性是用来告诉 IE 浏览器,即将获取的 JS 脚本代码没有 DOM 操作,因此 IE 浏览器在碰到这类 <script>
标签时不会阻塞后续DOM的的渲染。
async
作用是告诉浏览器异步获取 JS 文件,因此在下载的过程中也不会阻塞 DOM 渲染。不过一旦下载完成,会立即执行。
所以,如果我们只是使用了 async 属性,只是达到了异步下载的目的,并不能实际控制其执行的时期。
2.1.5 非常规方式
非常规方式指使用一些比较 Hack 的方式解决问题。比如上面我提到 <script>
标签用来引入 JS 文件,但是我这里偏不,
我这里就用 <object>
标签来引入 JS 文件。 <object> 标签原本的作用是嵌入多媒体资源,比如:视频,音频,PDF之类的。
或者使用 <img>
标签来引入 JS 文件。这些 Hack 的思路是想利用这些标签先获取一遍资源,然后期待浏览器能够将请求来的 JS 文件缓存进浏览器,
那么下次(就是使用 <script>
设置 src
属性引入资源)再次请求的时候,就没有网路开销了。
不过我们测试下来,这个跟浏览器的实现很有关系。在 Chrome(Version 54.0.2840.71 m (64-bit))
,我们并没有测试成功。使用 <object>
的表现是资源依然会被下载,并不是从缓存中获取; <img>
标签则是直接拒绝引入 JS 文件。
所以,我们不知道Javascript装载和执行1一文中最后使用 <object>
这个实验究竟正确性如何,欢迎各位探索一下。
2.2 执行
2.2.1 eval
通过 eval
函数运行字符级的 JS 代码,动态添加执行代码。不过使用 eval
函数,我的直觉是存在严重的性能和安全问题。
但究竟影响多少?为何会影响?等问题却不知道答案。那我凭什么要说这个不好?于是我找了一些文章,想看看具体情况是什么样子的。
看完这些文章之后,发现 eval
并不 evil
。(eval 的中文翻译:重新运算求出参数的内容,我之前一直以为是邪恶!)
2.2.2 script 文本
这种方式是将 JS 代码插入 <script type=text/javascript></script>
之间。让浏览器去执行。
这种方式我推荐如下代码实现:
var script = document.createElement('script') var text = document.createTextNode(/*your code*/) script.appendChild(text) script.type = 'text/javascript' document.body.appendChild(script) // delete the script element to keep the DOM Tree clean setTimeout(function () {document.body.removeChild(script);}, 10000)
创建一个 textNode
,以去除 JS Code 中可能存在的特殊字符。然后使用 appendChild
将其插入到 DOM 树中,
交给浏览器去执行。
3 尾声
通过2天时间不停的查资料,分析,测试,然后否定,重新查资料,分析,测试,然后否定的循环,最终确定了一个折中的方案:
通过 Ajax 技术异步获取 JS 文本代码,然后通过一个计数器确定最终调用回调的时机。之后把所获取到的 JS 文本代码 join
一下,
创建 textNode 节点,一次性全部插入到 DOM 树中执行,延迟几秒(目前设定是10秒)后将其删除。其实,这里可以优化一下,
最好能够做成:没有依赖的 JS 代码,或者是基础的 JS 代码能先执行的就让浏览器去先执行,然后需要依赖的等待依赖执行完成之后执行。
而不是等到所有代码全部下载完成之后再执行。
这样可以充分利用好所有的时间片。但是这个想法目前还没有写代码论证,是否对于提升 JS 整体运行效率有什么帮助还未得知。
这个过程中对于之前的一些错误观点进行了批判;对于浏览器底层对于 JS 的处理过程有了一个初步的了解。 同时,对于解决问题的思路又多了一些,尤其是看到不通过常规的方式下载 JS 代码时,更加的被其吸引。
在开发的过程中也遇到了一些以前没有碰到的 bug(或者叫做 feature)更加的合适?
IE系列浏览器对于 load
事件(资源加载完成之后浏览器发出的 load
事件)的处理比较的繁琐。
常规的注册方式我在这里就不提了,主要是你发现你注册了回调事件,但是并没有执行时,你可以试试这个样子:
element.setAttribute('load', your_callback_function_name) element.setAttribute('onreadystatechange', your_callback_function_name)
4 Other Reference
Footnotes:
Javascript 装载和执行 (陈皓)