家好,我是前端西瓜哥。今天我们来了解一下 script 脚本的三种加载方式。
一般的 script 写法为:
<script src="app.js"></script>
这种写法有一个问题:它会 阻塞 HTML 的 DOM 构建。
假如我们在 head 元素中使用了 script 脚本,它就会阻止后面元素的渲染,包括 body 元素,此时执行document.querySeletor('body') 拿到的是 null。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<script>
// 拿到 null
console.log(document.querySeletor('body'));
</script>
</head>
<body></body>
</html>
此外,当脚本足够大,加载执行足够久时,会导致页面长时间没能渲染出完整页面。
这也是我们将业务代码脚本放到 body 最下边的原因,这样能确保脚本能够访问一个完整的 DOM 树,也不会阻止页面的渲染。
缺点是,HTML 很长的时候,解析到脚本就会花上一点时间,然后才会请求对应的脚本资源。
不过通常来说,HTML 内容都比较简单,二者感受不到太大区别,除非你网很卡。
<script defer src="app.js"></script>
defer,“延迟” 之意。这里的延迟,指的是延迟执行脚本,下载则不会被阻塞。
需要注意的是, defer 属性对内嵌脚本无效。毕竟脚本内容就在 HTML 里了,完全不需要请求资源了好吧。
给 script 标签添加了 defer 属性后,脚本不会阻塞 DOM 树的构建,会先下载资源,然后等待到在 DOMContentLoaded 事件前执行。
DOMContentLoaded 事件的触发时机为初始 HTML 被构建完成时,此时 CSS、图片等资源不需要加载完,但我们的脚本要执行完。
如果多个 script 设置了 defer 属性,这几个 script 的执行顺序和声明顺序相同,即最前面的脚本先执行。并不是谁先下载谁先执行。
实际开发中,我们可以将业务代码脚本加上 defer 属性,放到更上层的 head 标签下。
这也是最新版 HtmlWebpackPlugin 插件的默认引入打包脚本的方式。
<script async src="app.js"></script>
async,“异步” 之意。同样对内嵌脚本无效。
设置 async 后,脚本一旦被下载好了就会执行,不管什么时机。
适合与执行顺序无关的脚本,比如广告、网站流量分析脚本。
比如插入 Google 分析脚本:
<script async src="//www.google-analytics.com/analytics.js"></script>
还有一种用脚本加载脚本的特殊情况,这里也说一说。
<script>
const script = document.createElement('script');
script.src = 'app-a.js';
document.body.appendChild(script);
</script>
脚本里创建一个 script 元素,设置好 src,然后加载到 DOM 树上,接着脚本就会下载和执行了。
创建的 script 元素默认会给 async 设置为 true,即一旦下载好就立即执行。
如果你要加载有依赖关系的多个脚本,就需要将 async 设置为 false。
<script>
const script = document.createElement('script');
// 取消 async 加载方式
script.async = false;
script.src = 'app-a.js';
document.body.appendChild(script);
const script2 = document.createElement('script');
script2.async = false;
script2.src = 'app-b.js';
document.body.appendChild(script2);
</script>
<script>console.log('我还是所有脚本中最先执行的')</script>
这样写,就能保证先执行 app-a.js,再执行 app-b.js
但 它无法做到比 HTML 中其他非动态加载的 script 脚本更早执行,这点需要注意。
script 有三种常见加载模式:
此外还有动态加载的脚本的情况,这种脚本默认为 async 加载形式,可通过将 async 属性设置为 false 来解除,让脚本顺序执行。
我是前端西瓜哥,欢迎关注我。
前端代码离不开浏览器环境,理解 js、css 代码如何在浏览器中工作是非常重要的。
如何优化渲染过程中的回流,重绘?script 脚本在页面中是怎么个加载顺序?了解这些对前端性能优化起着非常大的作用。
借着这篇文章,让自己对这块知识的理解更深一步。
浏览器通过解析 HTML 和 CSS 后,形成对应的 DOM 树和 CSSOM 树。
从根节点开始解析 DOM 树节点并匹配对应的 CSSOM 样式规则,选择可见的的节点,最终结合成一颗渲染树。
从上图能看到渲染树的特点:
根据上图,整个渲染阶段分为三部分:
两者的关系:触发回流一定会触发重绘, 而触发重绘却不一定会触发回流
下图很形象的展示了 Mozilla 页面的渲染过程。
都知道频繁的渲染过程会影响网页性能,但怎么知道网页开始渲染内容了呢?
我们可以通过 Chrome 的 F12,选择 Rendering 来查看网页的性能。
结合上面的方法,用 一个简单的 Demo 来示意:
能从图中看到,这些操作 触发了浏览器的重绘:
布局/回流 和 绘制/重绘 是页面渲染必须会经过的两个过程,不断触发它们肯定会增加性能的消耗。
浏览器会对这些操作做优化(把它们放到一个队列,批量进行操作),但如果我们调用上面提到的 offsetXX, clientXX,scrollXX,getClientRects 等属性方法就会强制刷新这个队列,导致这些队列批量优化无效。
下面列举一些简单优化方式:
注:均放在 head 标签内。
考个问题:CSS 定义在 head 中,其需加载 5 秒,请问页面加载后内容会先优先展示吗?
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 延迟5秒 -->
<link rel="stylesheet" href="/css/demo.css?t=5000" />
</head>
<body>
<div class="layout">我被渲染出来了</div>
</body>
</html>
我原先以为页面内容会优先渲染,CSS 加载完成后才改变内容样式。其实这是错的。
从上图看到,页面加载后,body 内元素就已经解析好了,只是没有渲染到页面上。随后 CSS 文件加载后,带有样色的内容才被渲染到页面上。
延迟的 link 的加载阻断了页面渲染,但并没有影响 HTML 的解析,当 CSS 加载后,DOM 完成解析,CSSOM 和 DOM 形成渲染树,最后将内容渲染到页面上。
反问,将 link 替换成 script 效果也一样吗?
与 link 不同,script 的加载会阻断页面 HTML 的解析,浏览器解析完 script 后,会等待 js 文件加载完后,页面才开始后续的解析,body 内容才出现。
学前端时相信都听过这样的名言:
CSS 写在 head 里,js 写在 body 结束标签前
知道了上面 link 和 script 的区别后,应该明白前半句的含义,下面来解释下后半句。
下面 script 均在 body 中。
先看下脚本在 body 中的一般情况:
在 body 内部的首位分别加载两个 js 文件,前者延迟 3 秒,后者延迟 5 秒,为了清楚他们的“工作”情况,在 head 中添加了定时器示意。
<html lang="en">
<head>
<script>
var t = 0;
var timer = setInterval(function () {
t++;
console.log('已加载 ', t, ' 秒');
if (t == 10) {
clearInterval(timer);
}
}, 1 * 1000);
</script>
</head>
<body>
<script>
var foo = 0;
console.log('init foo', foo);
</script>
<script src="/js/addTen.js?t=3000"></script>
<div>我被渲染了</div>
<script src="/js/addOne.js?t=5000"></script>
</body>
</html>
能看到 body 中定义的内联脚本首先工作,初始化 foo 变量。
随后加载 addTen.js,并阻断页面渲染。3 秒后,输出 js 内容(foo 赋值为 10),页面并重新开始解析,展示 div 内容。
最后加载 addOne.js ,继续等待 2 秒后,输出 js 内容(foo 赋值为 11)。
如果前一个 js 文件加载慢于后一个,会有怎么个效果?
<script src="/js/addTen.js?t=5000"></script>
<div>我被渲染了</div>
<script src="/js/addOne.js?t=1000"></script>
两个 script 标签并行加载,1 秒后 addOne.js 首先加载完毕,等待 4s 秒后,addTen.js 加载完后,页面直接渲染(因为 script 已经全部完成)。
所以建议 script 放在 body 结束标签之前,确保页面内容全部解析完成并开始渲染。
DOMContentLoaded 事件可以来确定整个 DOM 是否全部加载完成,下面我们简单测试下:
<script>
document.addEventListener('DOMContentLoaded', function () {
console.log('[ready] document');
});
</script>
<!-- ... -->
<script src="/js/addTen.js?t=5000"></script>
<div>我被渲染了</div>
<script src="/js/addOne.js?t=1000"></script>
最终输出:
addTen.js
foo 10
addOne.js
foo 11
[ready] document
DOMContentLoaded 事件的定义是异步回调方式,当 DOM 加载完成后触发,即使写在最前面,也会等待后面的 script 加载完成后才触发。
这里顺便提个 window.onload :
window.onload 和 DOMContentLoaded 不同,前者会等待页面中所有的资源加载完毕后再调用执行(比如:img 标签),后者在 DOM 加载完毕后即触发。
能看到无论 script 放在那个位置,浏览器都会等待他们直至 body 内的文件全部加载完。
那有什么 真正的异步 脚本加载吗?(不会阻断页面解析)
那就是 动态脚本。
如果你接触过第三方网页统计脚本,那将比较了解,下面给段示例代码:
<script>
document.addEventListener('DOMContentLoaded', function () {
console.log('[ready] document');
});
</script>
<script>
var newScript = document.createElement('script');
newScript.type = 'text/javascript';
newScript.src = '/js/dynamicScript.js?t=8000';
document.getElementsByTagName('head')[0].appendChild(newScript);
// 脚本加载完毕
if (newScript.readyState) {
newScript.onreadystatechange = function () {
if (newScript.readyState == 'loaded' || newScript.readyState == 'complete') {
console.log('dynamicScript.js loaded');
}
};
} else {
newScript.onload = function () {
console.log('dynamicScript.js loaded');
};
}
</script>
<script src="/js/addTen.js?t=5000"></script>
<div>我被渲染了</div>
<script src="/js/addOne.js?t=1000"></script>
最终输出:
addTen.js
afoo 10
addOne.js
foo 11
[ready] document
已加载 5 秒
已加载 6 秒
已加载 7 秒
已加载 8 秒
dynamicScript.js is running
dynamicScript.js loaded
已加载 9 秒
已加载 10 秒
定义了需要加载 8 秒的 dynamicScript.js 文件,所有的 script 加载方式依旧异步,但 dynamicScript.js 在 DOMContentLoaded 触发后,最后才执行,浏览器并没有等待它的加载完成后才渲染页面。
我们也可以将它放在 head 中。这种通过脚本来动态修改 DOM 结构的加载方式是 无阻塞式 的,不受其他脚本加载的影响。
我们可以在 script 定义 defer 、 async ,使整个脚本加载方式更加友好。比如:被修饰的脚本在 head 中,将不会阻断 body 内容的展示。
注意: defer 修饰的脚本将延迟到 body 中所有定义的脚本之后,DOM(页面内容)加载完之前触发; async 不会像 defer 一样等待 body 中的脚本,而是当前脚本一加载完毕就触发。
<head>
<!-- 如果上面没有其他响应慢的脚本,解析到此处加载完后将立马执行 -->
<script async src="/js/scriptAsync.js?t=3000"></script>
<!-- 1 秒,延迟到 DOM 加载完毕 -->
<script defer src="/js/scriptDefer.js?t=1000"></script>
</head>
<body>
<script>
document.addEventListener('DOMContentLoaded', function () {
console.log('[ready] document');
});
</script>
<script>
var newScript = document.createElement('script');
newScript.type = 'text/javascript';
newScript.src = '/js/dynamicScript.js?t=8000';
document.getElementsByTagName('head')[0].appendChild(newScript);
// 脚本加载完毕
if (newScript.readyState) {
newScript.onreadystatechange = function () {
if (newScript.readyState == 'loaded' || newScript.readyState == 'complete') {
console.log('dynamicScript.js loaded');
}
};
} else {
newScript.onload = function () {
console.log('dynamicScript.js loaded');
};
}
</script>
<script>
console.log('init foo', 0);
var foo = 0;
</script>
<script src="/js/addTen.js?t=5000"></script>
<div>我被渲染了</div>
<script src="/js/addOne.js?t=1000"></script>
</body>
加载顺序:
已加载 1 秒
已加载 2 秒
scriptAsync.js
已加载 3 秒
已加载 4 秒
addTen.js
foo 10
addOne.js
foo 11
scriptDefer.js
[ready] document
已加载 5 秒
已加载 6 秒
已加载 7 秒
已加载 8 秒
dynamicScript.js is running
dynamicScript.js loaded
已加载 9 秒
已加载 10 秒
本文使用 mdnice 排版
么是JS延迟加载?
JS延迟加载,也就是等页面加载完成之后再加载JavaScript文件
为什么让JS实现延迟加载?
js的延迟加载有助于提高页面的加载速度。
Js延迟加载的方式有哪些?一般有以下几种方式:
·defer属性
·async属性
·动态创建DOM方式
·使用jQuery的getScript方法
·使用setTimeout延迟方法
·让JS最后加载
HTML 4.01为<script>标签定义了defer属性。标签定义了defer属性元素中设置defer属性,等于告诉浏览器立即下载,但延迟执行标签定义了defer属性。
用途:表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕之后再执行在<script>元素中设置defer属性,等于告诉浏览器立即下载,但延迟执行
<!DOCTYPE html>
<html>
<head>
<script src="test1.js" defer="defer"></script>
<script src="test2.js" defer="defer"></script>
</head>
<body>
<!--这里放内容-->
</body>
</html>
说明:虽然<script>元素放在了<head>元素中,但包含的脚本将延迟浏览器遇到</html>标签后再执行HTML5规范要求脚本按照它们出现的先后顺序执行。在现实当中,延迟脚本并不一定会按照顺序执行defer属性只适用于外部脚本文件。支持HTML5的实现会忽略嵌入脚本设置的defer属性
HTML5 为<script>标签定义了async属性。与defer属性类似,都用于改变处理脚本的行为。同样,只适用于外部脚本文件。标签定义了async属性。与defer属性类似,都用于改变处理脚本的行为。同样,只适用于外部脚本文件。
目的:不让页面等待脚本下载和执行,从而异步加载页面其他内容。异步脚本一定会在页面 load 事件前执行。不能保证脚本会按顺序执行
<!DOCTYPE html>
<html>
<head>
<script src="test1.js" async></script>
<script src="test2.js" async></script>
</head>
<body>
<!--这里放内容-->
</body>
</html>
async和defer一样,都不会阻塞其他资源下载,所以不会影响页面的加载。
缺点:不能控制加载的顺序
//这些代码应被放置在</ body>标签前(接近HTML文件底部)
<script type="text/javascript">
function downloadJSAtOnload() {
varelement = document .createElement("script");
element.src = "defer.js";
document.body.appendChild(element);
}
if (window. addEventListener)
window.addEventListener("load" ,downloadJSAtOnload, false);
else if (window.attachEvent)
window.attachEvent("onload", downloadJSAtOnload) ;
else
window. onload =downloadJSAtOnload;
</script>
$.getScript("outer.js" , function(){ //回调函数,成功获取文件后执行的函数
console.log(“脚本加载完成")
});
<script type="text/javascript" >
function A(){
$.post("/1ord/1ogin" ,{name:username,pwd:password},function(){
alert("Hello");
});
}
$(function (){
setTimeout('A()', 1000); //延迟1秒
})
</script>
把js外部引入的文件放到页面底部,来让js最后引入,从而加快页面加载速度例如引入外部js脚本文件时,如果放入html的head中,则页面加载前该js脚本就会被加载入页面,而放入body中,则会按照页面从上倒下的加载顺序来运行JavaScript的代码。所以我们可以把js外部引入的文件放到页面底部,来让js最后引入,从而加快页面加载速度。
上述方法2也会偶尔让你收到Google页面速度测试工具的“延迟加载javascript”警告。所以这里的解决方案将是来自Google帮助页面的推荐方案。
//这些代码应被放置在</body>标签前(接近HTML文件底部)
<script type= "text/javascript">
function downloadJSAtonload() {
var element = document.createElement("script");
element.src = "defer.js";
document.body.appendChild(element);
}
if (window.addEventListener)
window.addEventListener("load", downloadJSAtOnload, false);
else if (window.attachEvent )
window.attachEvent("onload", downloadJSAtonload);
else window.onload = downloadJSAtOnload;
</script>
这段代码意思等到整个文档加载完后,再加载外部文件“defer.js”。
使用此段代码的步骤:
6.1)复制上面代码
6.2)粘贴代码到HTML的标签前 (靠近HTML文件底部)
6.3)修改“defer.js”为你的外部JS文件名
6.4)确保文件路径是正确的。例如:如果你仅输入“defer.js”,那么“defer.js”文件一定与HTML文件在同一文件夹下。
注意:
这段代码直到文档加载完才会加载指定的外部js文件。因此,不应该把那些页面正常加载需要依赖的javascript代码放在这里。而应该将JavaScript代码分成两组。一组是因页面需要而立即加载的javascript代码,另外一组是在页面加载后进行操作的javascript代码(例如添加click事件。
*请认真填写需求信息,我们会在24小时内与您取得联系。