UI线程的阻塞.

 

在开始前我们先看一道题目, 如果你心中的答案是正确的,那么可能此文的内容大多你已经心里有数了.


<body>
.....1000行div 在这里......
<p>franky</p>
<script>
    var t = new Date;
     while (new Date - t < 5000);
</script>
</body>


<body>
<script>
    var t = new Date;
     while (new Date - t < 5000);
</script>
<p>franky</p>
.....1000行div 在这里......
</body>
  

 

其他环境假设都一致的情况下,且假设franky能在第一屏显示。能让我们更早看到 franky 这段文字的写法是哪一种?


b.js code:

var t = new Date;
while(new Date - t < 5000);


测试1:

<div id="d1" style="width:200px;height:200px;border:solid 1px #000;">123123123123</div>
<script>
var t = new Date;
while(new Date - t < 3000);
</script>
结果:
除了opera9.2+(再早期的没有测试) 外,所有浏览器都没有第一时间渲染div.以及其内部的 文本. 而是等到退出script执行期以后才去渲染.
而opera也不是第一时间渲染.而是大概阻塞了1.5秒左右. 原因不明.
额外做一个测试,如 在script标签与 div中间 插入了5000行的 div 和p 以及一些文字节点. 但测试结果同上.

 

测试2 :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>test</title>
</head>
<body>
<p>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</p>
<p>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</p>
<p>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</p>
<script>
     document.body.style.background = 'red';
    
alert(1); //看看注释掉alert与不注释掉的区别.
</script>
<p>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</p>
<p>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</p>
<p>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</p>
<script src="b.js"></script>
<p>cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc</p>
<p>cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc</p>
<p>cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc</p>
</body>
</html>
 
 
总结:
     一般来说,除了opera以外浏览器第一次的渲染是要DOM Tree 构建完毕后 构建出完整的Render Tree后,才进行的. 当然 ,如果中途某个脚本执行了如 alert等语句. 则会导致浏览器把之前的ui update队列全部flush并渲染.否则,DOM Tree, Render Tree 不构建完毕前,不进行第一次的渲染. alert之所以引起提前渲染.本质原因是,alert 弹窗也是一种 ui update.行为.而且为了及时反馈用户alert弹窗.这里浏览器们强制flush了 ui update队列.进行了渲染. IE 看起来直接调用 MsgBox 不应该和 ui线程扯上什么关系.  但至少测试结果告诉我们. ie系.在调用alert方法时,并没有仅仅调用MsgBox了事.而是先flush掉当前ui update队列中等待渲染的工作.
     一些特殊的情况下如当DOM Tree很大很长的时候(考虑上万个节点,或更多.但不绝对,DOM Tree的复杂性,会大大缩小这个条件的重要性.).  浏览器会根据情况,适当的提前首次渲染的时间.具体算法不明.且各个浏览器的策略也有区别. 但参考的项大概涉及到 DOM Tree 的复杂度 、庞大程度、DOM Tree构建消耗的时间. 外部资源加载,甚至是DOM Tree构建,被阻塞的次数.等等等等

  考虑如下脚本在FireFox中的特殊表现:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>test</title>
</head>
<body>
123123123123
<script>
var t = new Date;
while(new Date - t < 1000);
</script>
123123123123
<script>
var t = new Date;
while(new Date - t < 1000);
</script>
123123123123
<script>
var t = new Date;
while(new Date - t < 1000);
</script>
123123123123
<script>
var t = new Date;
while(new Date - t < 1000);
</script>
123123123123
<script>
var t = new Date;
while(new Date - t < 1000);
</script>
123123123123
<script>
var t = new Date;
while(new Date - t < 1000);
</script>
123123123123
<script>
var t = new Date;
while(new Date - t < 1000);
</script>
123123123123
<script>
var t = new Date;
while(new Date - t < 1000);
</script>
123123123123
<script>
var t = new Date;
while(new Date - t < 1000);
</script>
123123123123
<script>
var t = new Date;
while(new Date - t < 1000);
</script>
</body>
</html>

  FireFox2.0 与FireFox 4.0 Beta8  以及其他浏览器表现一致.阻塞10秒 一次性渲染所有文本.
  FireFox 3.6.1.3 表现为 首次渲染 2秒后 一次性出现两组数字文本.其后每隔1秒渲染一组数字文本.
  FireFox3.0与FireFox3.5 表现有趣.经过多次测试.表现出一种不稳定性.有时等待几秒渲染一次.有时会有连续的一秒渲染一次的现象.
 

补充 :
     setTimeout可以缓解此问题. 尤其ie 系. setTimeout(fn);即可,即回调间隔实际为最低系统时钟间有效隔值.
其他浏览器,则有以下关系. 中间节点 或者DOM Tree 或者 Render Tree 或者更直白些叫 预渲染的内容. 这些东西的数量和setTimeout的间隔值成正比.
但最低不要低于浏览器的最低有效系统时钟间隔值.建议20秒(凑个整).  如果内容很多.则setTimeout 的值可能要超过250.此值是老外提出的一个很吉利的数值.综合考虑首屏文字、图片等资源的提前渲染.
     chrome比较新版本,可以使用 scriptElement 动态加载方式.把执行时间过长的脚本放到外部引入,来解决此问题. 
     除FireFox外的其他浏览器,则有附加条件,即.仅当这个脚本加载很慢(相对 DOM Tree的构建).而由于动态加载.不阻塞DOM Tree构建.使浏览器更早的结束了 DOM Tree 和 Render Tree构建并 进行了首次的页面渲染.
     FireFox.动态scriptElement方式对此问题没有任何帮助.
  
  

 

测试3 :关于chunked 分段输出.
<?php
header('Cache-Control:no-store');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>test</title>
</head>


<body>
<p>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</p>
<p>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</p>
<p>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</p>

<?php
     flush();
?>
<script src="//www.a.com/b.js">
</script>

<p>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</p>
<p>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</p>
<p>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</p>
</body>
</html>
结果:在模拟网络延迟的慢网速环境下.chuked.非常好的解决了 b.js阻塞 ui update 队列的渲染. 而直接在本地iis访问的情况下由于包含b.js的分段过早被浏览器接收到,则chunked变的毫无意义.
 
 
 
chuked 分段输出的意义:
诚然. 这个可以解决一般性的 脚本执行期阻塞 浏览器ui update 的问题.使得脚本执行仅仅阻塞其后面的 DOM Tree 构建 .
但其本质是.当浏览器尝试提前渲染既有的Render Tree中内容(分段输出,使浏览器提前渲染.)的时候. 因为文档流中没有scrip被parse. 则此时浏览器会正常的根据Render Tree 做渲染工作. 但应该注意的问题是.如果第二段具备script标签的文档流分段,被浏览器过早接收到.即当浏览器做提前渲染时,已经接收到下一段包含script的文档流,并对其进行了HTML Parse后. 浏览器就会停止准备好的渲染工作. 去加载执行script. 最终阻塞ui update ,其具体行为参考前两个测试.
 
 
参考的建议:
对于HTML 文档.务必使用分段输出的同时,把脚本资源放在越靠后的段落越好. 即对于一个完整的html文档来说, 脚本越靠后越好的另一种诠释. 如果具备一个执行时间比较长的脚本.尽量使用setTimeout + Web Worker 代替直接执行.
现在,对于下面的代码.不做测试的话,你能说出各个浏览器的具体行为么?
 
 
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>test</title>
</head>
<body>
<script>
    document.body.style.background = 'red';
</script>
<p>其实我的存在并不重要,你懂得.</p>
<script>
var t = new Date;
while(new Date - t < 5000);
</script>
</body>
</html>

 

再看下IE系与众不同的地方:
 
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>test</title>
</head>
<body>
<script>
document.body.style.background = 'red';
setTimeout(function(){
    var t = new Date;
    while(new Date - t < 5000);
});
</script>

</body>
</html>

 

此时唯独ie仍然可以解决,阻塞 ui update的行为. 也就是说. ie系的定时器回调函数的权限没有ui update高.
其他浏览器呢?还记得前面提到的,史蒂夫说过的神奇的250么?是的.这是个万金油.
事实上是. 除了opera以外,下面的代码,顺序都是先打印2 后打印1的 :
 

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>test</title>
</head>
<body>
<script>
setTimeout(function(){
   alert(1);
});
</script>

<div class="abc">后面还可以有1万行各种各样的div和内容</div>
<div...
<div...
<div...

<script>
    alert(2);
</script>
</body>
</html>

当然ie6 偶尔会体现出一种不稳定性.即偶尔的时候 清除掉缓存,并关闭浏览器, 作为第一次访问此测试页面时, ie6 有时候会先打印1,后打印 2.这起初是我无法理解的事情.
我是不是可以大胆猜测一下.ie6可能会缓存曾经访问过的某个页面的 DOM Tree.并在你刷新页面时,会直接使用原有的DOM Tree的全部或部分. 这样导致ie6 可以知道 1万行div的后面,有个script标签.在他解析执行完之前.不应该渲染页面.更不应该执行定时器回调. 而仅当初次访问这个陌生的页面的时候.IE6可能在解析了足够长的时间后.仍然没有解析到文档流的结束.  也许是出于优化,提前渲染页面的考虑. 或者其他什么原因.导致 IE6提前渲染了页面.并先执行了定时器回调,打印1.然后我们才看到2被迟迟的打印了出来. 当然这一切,仅仅是猜测而已.


那么,最后我们在看看本文最开始的那个问题,你的答案改变了么?
posted @ 2010-12-31 18:33  Franky  阅读(8327)  评论(14编辑  收藏  举报