#### 1.defer与async的分析 ##### 1.1 defer,async与保证js加载完成后才调用的实现 ```html ``` 没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。 ```html ``` 有async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。 ```html ``` 有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在`所有元素解析完成之后,DOMContentLoaded 事件触发之前`完成。 然后从实用角度来说呢,首先把所有脚本都丢到 <\/body>(body结束符) 之前是最佳实践,因为对于旧浏览器来说这是唯一的优化选择,此法可保证非脚本的其他一切元素能够以最快的速度得到加载和解析。参见下图:  defer和async在`网络读取(下载)这块儿`是一样的,都是异步的(相较于 HTML 解析) 它俩的差别在于脚本下载完之后何时执行,显然defer是最接近我们对于应用脚本加载和执行的要求的。 async则是一个`乱序执行`的主,反正对它来说脚本的加载和执行是紧紧挨着的,先下载完成的脚本必先执行。然而,因为浏览器本身是[单线程,只有一个调用栈](https://github.com/liangklfangl/react-article-bucket/blob/master/others/nodejs-QA/browser-QA.md),所以当js下载完后执行的过程中页面DOM解析依然是阻塞的,不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行。最典型的例子:[`Google Analytics`](https://developers.google.cn/analytics/devguides/collection/analyticsjs/command-queue-reference#ready-callback)比如下面的例子: ```js // window,document,'script','//www.google-analytics.com/analytics.js','ga' (function(i, s, o, g, r, a, m) { i['GoogleAnalyticsObject'] = r; // 1.window['GoogleAnalyticsObject'] = 'ga'; i[r] = i[r] || function() { (i[r].q = i[r].q || []).push(arguments) }, i[r].l = 1 * new Date(); /* 2. 在 JavaScript 中,函数也是对象,这意味着函数中也可以包含属性。跟踪代码段在 ga函数对象上定义了一个值为空数据的 q 属性。在 analytics.js 库尚未加载完成之前,调用 ga() 函数会将传递给 ga() 函数的参数列表附加到 q 数组的尾部。analytics.js 加载完成(onload)后,会立即查看 ga.q 数组的内容并依次执行每条命令。然后,ga() 函数将被重新定义以立即执行之后的调用。 * window['ga'] = window['ga'] || function(){ * window['ga'].q = (window['ga'].q || []).push(arguments) * }, * window['ga'].l = 1 * new Date(); */ a = s.createElement(o), m = s.getElementsByTagName(o)[0]; /**3. * a = document.createElement('script'), * m = document.getElementsByTagName('script')[0] */ a.async = 1; /**4.async表示加载完成后就直接执行,此时DOM可能并没有解析完毕,可以继续往下解析。但是我们后续直接调用了create,send方法,此时后续分析代码可能并没有下载完成,所以会直接push到队列里面,如果onload触发了就会执行队列里面的回调并重置相应方法。 * script.async = 1; */ a.src = g; /**5. * script.src = '//www.google-analytics.com/analytics.js' */ m.parentNode.insertBefore(a, m) /**6. Insert Analytics Script Before First Script Tag ! * document.getElementsByTagName('script')[0].parentNode.insertBefore(document.createElement('script'),document.getElementsByTagName('script')[0]) */ })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); ga('create', 'UA-72788897-1', 'auto'); ga('send', 'pageview'); /**7. * 在第一条命令中,create 接受了通过第二个、第三个和第四个可选参数指定的相应 trackingId、cookieDomain 和 name 字段。send 命令接受通过第二个可选参数指定的 hitType。 * 所有命令均接受普遍适用的 fieldsObject 参数,该这种参数可用于指定任何字段。例如,可将上述跟踪代码段中的两条命令改写为: ga('create', { trackingId: 'UA-XXXXX-Y', cookieDomain: 'auto' }); ga('send', { hitType: 'pageview' }); */ ``` 。至于defer和async的支持情况可以使用[caniuse](https://caniuse.com/#search=defer)进行查看。 ##### 1.2 单页面例子优化 ```html ``` 上面的例子常见于单页面应用,其中common.js和index.js来自于Webpack的打包文件,而上面的其他的js都是单页面的第三方类库,那么上面的资源加载顺序有没有问题呢?下面[分析](https://github.com/liangklfangl/react-article-bucket/blob/master/chrome-core/webCore/webkit-render-process.md#22-dom%E5%81%9C%E6%AD%A2%E6%9E%84%E5%BB%BA%E4%BD%86%E6%98%AF%E8%B5%84%E6%BA%90%E7%BB%A7%E7%BB%AD%E5%8A%A0%E8%BD%BD)下:
1.推测加载策略在network面板很容易就看到了,所以js文件不阻塞后面js文件的加载 2.除了react-dom,react是后续common.js和index.js渲染必须的资源以外,其他资源都可以异步加载,即非关键路径资源##### 1.3 前端性能 - 白屏时间(first Paint Time) 用户从打开页面开始到页面开始有东西呈现为止。 - 首屏时间 用户浏览器首屏内`所有内容都呈现出来`所花费的时间 - 用户可操作时间(dom Interactive) 用户可以进行正常的点击、输入等操作,默认可以统计`domready`时间,因为通常会在这时候绑定事件操作 - 总下载时间 页面所有资源都加载完成并呈现出来所花的时间,即页面`onload`的时间 ##### 1.4 标签位置与页面呈现First Paint(web页面什么时候对用户可见) 按照传统的做法,所有的script元素都应该放在页面的head元素中,例如: ```html
(1)document.write在XHTML中不适用 (2)document.write只有在页面加载(onload之前)的情况下适用,否则会重写整个页面 (3)document.write在遇到的时候就会执行,不能在指定的Node中插入元素 (4)document.write写入的都是序列化的文本,这和操作DOM的方式还是有区别的。容易产生bug但是这种方式对于如Google Analytics来说是最好的。因为他们不用担心该方法会覆盖原有的页面的onload事件,或者想方设法去添加自己的onload事件。因为遇到该脚本后就会异步加载它并执行,而且这种方式浏览器兼容也比较好。 ```html
Hello world! This is HTML5 Boilerplate.
``` 更多讨论你可以[查看这个](https://stackoverflow.com/questions/802854/why-is-document-write-considered-a-bad-practice)讨论。 #### 5.iframe相关内容总结 ##### 5.1 iframe创建成本很高  通过上图知道:创建100个不同类型的元素,创建iframe的时间花销是创建如script,style标签的1-2个量级。虽然我们页面中并不会有如此多的iframe,但是从另一方面来说iframe的时间花销确实要比普通元素高得多。 ##### 5.2 Iframes阻塞主页面的onload window的onload方法应该尽快触发,这样浏览器很多与[加载中](https://www.safaribooksonline.com/library/view/even-faster-web/9780596803773/ch04.html)相关的图标就会停止,比如status bar,cursor, tab icon,progress bar等。同时该事件的触发也能让用户知道当前页面已经加载完成,如果onload迟迟未触发就会让用户感觉当前的页面加载缓慢。 window的onload事件必须等到iframe中所有的资源都加载完成才行。在SF和Chrome中,动态设置iframe的src的值能够避免这种阻塞行为。 ##### 5.3 和主页面共享连接池 浏览器针对某一个服务器只会打开有限的链接,比如一些老的浏览器,包括[IF6&7](http://www.stevesouders.com/blog/2008/03/20/roundup-on-parallel-connections/),FF2等只会针对一个域名打开两个连接。当然针对新的浏览器比如SF3或者Opera 9+针对一个域名会打开4个连接,而Chrome 1+,,IE 8,FF3会打开[6个连接](chrome://net-internals/#events)。 我们可能期望iframe有自己的连接池,但是现实是残酷的。对于大多数的浏览器两者**共享**同一个连接池,这也就是意味着iframe可能占用所有的可用连接进而阻塞主页面资源的加载。 如果iframe中的资源和主页面中资源同样重要,或者更重要,那么并没有问题,反之就不合理了。当然,这个问题可以通过当重要资源加载完毕后动态设置iframe的src来解决 #### 5.4 iframe跨域通信 对于现代浏览器,postMessage API还是无可撼动的。IE6/7下,使用的是一个被认为是bug或安全漏洞的特性,即navigator对象在父窗口和iframe之间是**共享**的。基于这一点,我们可以在父窗口中,在navigator对象上注册一个消息回调函数;在iframe中,调用navigator上的这个函数并传入参数。此时可看作,iframe往父窗口的一个函数传递了一个参数,并在父窗口的上下文中执行了,那么就相当于iframe向父窗口发送了一条消息。反之亦然。 这种方式的好处也是很明显的:(1)该方案不依赖浏览器的各项设计,不受设置影响,同时完美支持HTTPS (2)不用创建多余iframe,基于接口调用,不需要轮询,性能大幅提升 (3)良好的接口封装,所有窗口对象统一对待 (4)多iframe也不怕,navigator对象的共享,让iframe之间直接通信成为可能下面是具体的代码: ```js window.Messenger = (function(){ // 消息前缀, 建议使用自己的项目名, 避免多项目之间的冲突 // !注意消息前缀应使用字符串类型 var prefix = "[PROJECT_NAME]", supportPostMessage = 'postMessage' in window; // Target 类, 消息对象 function Target(target, name, prefix){ var errMsg = ''; if(arguments.length < 2){ errMsg = 'target error - target and name are both required'; } else if (typeof target != 'object'){ errMsg = 'target error - target itself must be window object'; } else if (typeof name != 'string'){ errMsg = 'target error - target name must be string type'; } if(errMsg){ throw new Error(errMsg); } this.target = target; this.name = name; this.prefix = prefix; } // 往 target 发送消息, 出于安全考虑, 发送消息会带上前缀 if ( supportPostMessage ){ // IE8+ 以及现代浏览器支持 Target.prototype.send = function(msg){ this.target.postMessage(this.prefix + '|' + this.name + '__Messenger__' + msg, '*'); }; } else { // 兼容IE 6/7 Target.prototype.send = function(msg){ // 主页面注册了事件到window.navigator上,iframe调用send方法时主页面调用 // window.navigator上的方法 var targetFunc = window.navigator[this.prefix + this.name]; if ( typeof targetFunc == 'function' ) { targetFunc(this.prefix + msg, window); } else { throw new Error("target callback function is not defined"); } }; } // 信使类 // 创建Messenger实例时指定, 必须指定Messenger的名字, (可选)指定项目名, 以避免Mashup类应用中的冲突 // !注意: 父子页面中projectName必须保持一致, 否则无法匹配 function Messenger(messengerName, projectName){ this.targets = {}; this.name = messengerName; this.listenFunc = []; this.prefix = projectName || prefix; this.initListen(); } // 添加一个消息Target对象 Messenger.prototype.addTarget = function(target, name){ var targetObj = new Target(target, name, this.prefix); this.targets[name] = targetObj; }; // 初始化消息监听,IE6&7通过window.navigator绑定事件 Messenger.prototype.initListen = function(){ var self = this; // 接受到消息的值 var generalCallback = function(msg){ if(typeof msg == 'object' && msg.data){ msg = msg.data; } var msgPairs = msg.split('__Messenger__'); var msg = msgPairs[1]; var pairs = msgPairs[0].split('|'); var prefix = pairs[0]; var name = pairs[1]; for(var i = 0; i < self.listenFunc.length; i++){ if (prefix + name === self.prefix + self.name) { self.listenFunc[i](msg); } } }; if ( supportPostMessage ){ if ( 'addEventListener' in document ) { window.addEventListener('message', generalCallback, false); } else if ( 'attachEvent' in document ) { window.attachEvent('onmessage', generalCallback); } } else { // 兼容IE 6/7 // window.navigator上绑定回调事件 window.navigator[this.prefix + this.name] = generalCallback; } }; // 监听消息 Messenger.prototype.listen = function(callback){ var i = 0; var len = this.listenFunc.length; var cbIsExist = false; for (; i < len; i++) { if (this.listenFunc[i] == callback) { cbIsExist = true; break; } } if (!cbIsExist) { this.listenFunc.push(callback); } }; // 注销监听 Messenger.prototype.clear = function(){ this.listenFunc = []; }; // 广播消息,遍历Target并发送消息 Messenger.prototype.send = function(msg){ var targets = this.targets, target; for(target in targets){ if(targets.hasOwnProperty(target)){ targets[target].send(msg); } } }; return Messenger; })() ``` #### 5.5 iframe阻塞主页面onload事件的解决方法 [iframe加载性能提升](http://www.aaronpeters.nl/blog/iframe-loading-techniques-performance?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed:+aaronpeters+(Aaron+Peters))的文章指出了好几种提升iframe性能的方法,其中大部分方法的不足在于会阻塞主页面的onload事件,同时会显示资源加载中,使得用户感知网页加载非常慢。比如[这个例子](./examples/dynamic-insert.html): ```html ```` 这个方法的明显之处在于:主页面onload后才创建一个iframe,通过该iframe去加载指定的资源。我们首先看看页面加载的瀑布流:  很显然瀑布流也显示onload后才去加载iframe内容。但是该方法有一个明显的不足:Chrome中DOMContentLoaded(蓝色的线)早已显示出来,但是在iframe加载的过程中上面的onload句柄createIframe虽然已经被调用了(打印了log),但是浏览器的onload线(红线)并没有绘制出来,同时浏览器一直显示有资源在**加载中**的图标。文中最后提供了一个方法: ```js ``` 完整的实例代码[点击这里](./examples/dynamic-async.html),此时我们可以查看瀑布流:  你会发现页面图片早早的下载完了,同时onload也已经触发(但是红线在iframe中资源加载完成后chrome才绘制出来),页面**不再显示资源加载中**,而js文件在onload后才开始加载。这样的页面会使得用户感觉到明显的速度加快。看到这里是不是幡然醒悟,这不是就前面我说的"Script in Iframe"吗?该js和主页面的内容就是并行加载的,同时也不会阻塞主页面的onload事件。 #### 6.onload事件所有的资源都加载完成了吗  其实蓝线表示DOMContentLoaded,而红线表示onload被触发。那我的问题是:onload触发后所有的资源就已经确定加载完了吗?我认为答案是否,我们看看百度首页的加载瀑布流:  在onload后依然有资源在加载,一般表示使用的是**js动态加载的资源**,而且这些资源并没有阻塞主页面的onload事件(比如上面iframe的onload未阻塞主页面onload的情况)。这些资源加载完成后会造成页面的重绘或者重排。所以,当在网速特别慢的情况下,你会发现页面部分绘制出来的情况。 #### 7.display:none的元素在DOM树中吗? 答案:**在**!如果这个元素本身不在DOM树中,那么通过document.getElementById这种方式来获取它根本就获取不到。但是值得注意的是:该元素并[**不在渲染树中**](../chrome-core/webCore/webkit-render-process.md),渲染树中需要展示的是那些可见的元素,除了display:none以外,比如head等也不会出现在渲染树中。这样,当通过设置display:block让display:none的元素可见的时候,浏览器就需要重新计算每一个元素在页面中的位置,即产生回流reflow操作,因为元素的状态从不在渲染树改成需要出现在渲染树中了。 讲到这里牵涉到了重绘和重排的概念,重排需要[**重新构建RenderObject树**](../chrome-core/webCore/webkit-render-process.md)(可能是DOM树改变或者[CSSOM](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/constructing-the-object-model),比如display改变了),然后重新计算改变的元素在窗口中最新的位置,最后将合成的bitmap绘制到后端存储并上传到显存中由显示器进行绘制。而如果是重绘,比如背景色的改变,那么可以省去最新位置的计算,因为元素没有因为尺寸的变化而导致其具体的位置发生变化,这样就减少了更新元素位置从而导致的回流reflow操作。下面是某一个具体的CSSOM树:  其对应的CSS为: ```css body { font-size: 16px } p { font-weight: bold } span { color: red } p span { display: none } img { float: right } ``` 其HTML结构为: ```html
Hello web performance students!

z-index值不为auto的flex项(父元素display:flex|inline-flex). 元素的opacity值不是1. 元素的transform值不是none. 元素mix-blend-mode值不是normal. 元素的filter值不是none. 元素的isolation值是isolate. will-change指定的属性值为上面任意一个。 元素的-webkit-overflow-scrolling设为touch.##### 9.3 父子关系与层叠顺序 层叠顺序表示元素发生**层叠时候有着特定的垂直显示顺序**:  其中关键信息如下:
(1)位于最低水平的border/background指的是层叠上下文元素的边框和背景色(比如下例的box > div元素,可能是某一个层叠上下文的父级元素)。每一个层叠顺序规则适用于一个完整的层叠上下文元素。 (2)inline-block和inline水平元素是同等level级别。 (3)z-index:0实际上和z-index:auto单纯从层叠水平上看,是可以看成是一样的。注意这里的措辞——“单纯从层叠水平上看”,实际上,两者在层叠上下文领域有着根本性的差异。因为后者不会创建层叠上下文。既然表示的是发生重叠时候的顺序,那么对于**父子元素**本身也会存在此类情况,比如下面的例子: ```html
1.创建层叠上下文的元素的背景和边界; 2.z-index为负值的子元素,数值越小越早被绘制; 3.同时满足“in-flow”、“non-inline-level”、“non-positioned”的后代元素; 4.“non-positioned”的浮动元素; 5.满足“in-flow”、“inline-level”、“non-positioned”的后代元素; 6.层叠级数为0的子层叠上下文以及“positioned”且层叠级数为0的后代元素; 7.层叠级数大于等于1的“positioned”子层叠上下文,数值越小越早被绘制;##### 10.事件处理函数中的[passive:true](http://blog.csdn.net/shenlei19911210/article/details/70198771)配置 由于浏览器无法预先知道一个事件处理函数中会不会调用preventDefault(),它需要**等到事件处理函数执行完后,才能去执行默认行为**,然而事件处理函数执行是要耗时的,这样一来就会导致页面卡顿,可以动手试试,比如在事件处理函数里面写一个耗时的循环。 其实passive就是为此而生的。设置了passive:true的情况下,即使滚动事件里面写了一个死循环,浏览器也能够正常处理页面的滑动。在DOM的最新规范中,事件处理函数的第三个参数变成了一个对象: ```js target.addEventListener(type, listener[, options]); ``` 我们可以通过传递passive为true来明确告诉浏览器,事件处理程序不会调用preventDefault来阻止默认滑动行为。此时浏览器就能**快速生成事件(滚动事件)**,从而提升页面性能,而不用等待滚动事件处理完成后才触发。 ##### 11.[MessageChannel](http://www.zhangxinxu.com/wordpress/2012/02/html5-web-messaging-cross-document-messaging-channel-messaging/)通道通信 消息通道提供了一个**直接,双向浏览上下文之间**的通信手段。跟跨文档通信一样,DOM不直接暴露。取而代之,管道每端为端口,数据从一个端口发送,另一个变成输入(反之亦然)。消息通道是有用的,**特别是跨多个起源的沟通**。请考虑以下情形:人人网上(http://renren.com)嵌入了一个第三方的游戏页面(通过iframe的形式,如“人人餐厅”),同时,这个第三方的游戏页面(http://game.com)又需要从另外一个通讯录网站(http://address.com)获取用户的通讯信息。咋办? 也就是说通讯录站点要发送信息给游戏站点,根据跨文档通信,我们让父页面作为代理(也就是这里的人人网页面)。然而,这种做法意味着**通讯录站点需要有和人人网页面一样的信任级别**。人人网这个社交站点需要信任每一个请求,或者为我们过滤(应该指:一个一个指定)。但是,使用渠道通信(MessageChannel),通讯录站点(http://address.com)和游戏站点(http://game.com)可以直接沟通。 主页面的js代码如下: ```js // 监听从右侧框架传来的信息 window.addEventListener('message', function(evt) { if (evt.origin == 'http://www.zhangxinxu.com') { if ( evt.ports.length > 0 ) { // 将端口转移到其他iframe文档 window.frames[0].postMessage('端口打开','http://www.zhangxinxu.com', evt.ports); } } }, false); ``` 左侧iframe的内容如下: ```js var eleForm = document.querySelector("form"), port; eleForm.onsubmit = function() { var message = document.querySelector("input[type='text']").value; if (port === undefined) { alert('信息发送失败,目前没有可用端口!'); } else { port.postMessage(message); } return false; }; window.addEventListener('DOMContentLoaded', function(e) { window.addEventListener('message', function(evt) { // 扩大端口范围,该iframe接受到port后保存下来用于和其他iframe通信 if (evt.origin == 'http://www.zhangxinxu.com') { port = evt.ports[0]; } else { alert(evt.origin +'这厮我不认识哈!'); } }, false); window.parent.postMessage('发送页加载完毕', 'http://www.zhangxinxu.com'); } ,false); ``` 而右侧iframe的代码如下: ```js var eleBox = document.querySelector("#message"); var messageHandle = function(e) { eleBox.innerHTML = '接受到的信息是:' + e.data; }; window.addEventListener('DOMContentLoaded', function() { if (window.MessageChannel) { // 创建一个新的 MessageChannel 对象 var mc = new MessageChannel(); // 给父级发送一个端口,这个端口会通过父级转发给其他的iframe // 进而实现两个iframe之间的直接通信 window.parent.postMessage('显示页加载完毕','http://www.zhangxinxu.com',[mc.port1]); // 接受从其他iframe通过port1发送过来的消息,显示发送的信息 mc.port2.addEventListener('message', messageHandle, false); mc.port2.start(); } else { eleBox.innerHTML = '搞咩乃赛,您的浏览器不支持通道通信。'; } }, false); ``` 而其与postMessage的区别如下(来自于[stackoverflow](https://stackoverflow.com/questions/37539941/stuck-in-postmessage-and-messagechannel)):
MessageChannel is basically a 2-way communication pipe. Think of it as an alternative to window.postMessage / window.onmessage - but a somewhat easier and more configurable.#### 12.nodejs中process.nextTick如何实现在浏览器中 Nodejs中有一个process.nextTick方法用于在下次事件循环空闲的时候指定某个函数。浏览器端一般都用setTimeout(0)去实现该功能,但是setTimeout(0)并不会立即执行函数,而是依然会等待一定时间。即所谓的setTimeout的4ms延迟。而这可能会导致一定的性能问题。[作者](http://www.nonblocking.io/2011/06/windownexttick.html)使用了在worker,iframe,当前window中postMessag的方法来模拟尽快执行一个函数,最后得到结论:**在当前window中postMessage是模拟process.nextTick的最佳方法**。每一种方式的代码实现如下: ```js var echoSetTimeout = (function() { return function(msg, cb) { setTimeout(cb, 0); } })(); /** * https://www.cnblogs.com/zoho/archive/2012/05/27/2520468.html * 利用 W3C 草案中的 Blob,我们有了新的方法来保存本地文件 * * echoWorker(payload.shortString, function resolve() { * deferred.resolve(); * }); */ var echoWorker = (function() { var code = 'onmessage = function(e) { postMessage(e.data) };'; var blobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder; if(!blobBuilder) { return echoSetTimeout; // 直接返回setTimeout(0) } var bb = new (blobBuilder)(); bb.append(code); var blobURL = (window.URL || window.webkitURL || window.mozURL).createObjectURL(bb.getBlob()); // 得到blockURL类型并创建worker对象 var worker = new Worker(blobURL); // 主页面调用worker.postMessage通知worker,并通过worker.onmessage监听worker发送过来的数据 return function(msg, cb) { worker.onmessage = cb; worker.postMessage(msg); }; })(); /** * iframe设置postMessage方式,给iframe传递一个消息并在主页面监听window.onmessage事件 * * 调用方式如下: * * echoIFramePostMessage(payload.shortString, function resolve() { * deferred.resolve(); * }); */ var echoIFramePostMessage = (function() { var iframe = document.createElement('iframe'); window.onload = function() { iframe.style.display = 'none'; document.body.appendChild(iframe); iframe.contentDocument.write('