几乎每一个前端程序员都知道应该把script标签放在页面底部。关于这个经典的论述可以追溯到Nicholas的 High Performance Javasript 这本书的第一章Loading and Execution中,他之所以建议这么做是因为:
复制代码 简而言之,如果浏览器加载并执行脚本,会引起页面的渲染被暂停,甚至还会阻塞其他资源(比如图片)的加载。为了更快的给用户呈现网页内容,更好的用户体验,应该把脚本放在页面底部,使之最后加载。 为什么要在标题中使用“再”这个字?因为在工作中逐渐发现,我们经常谈论的一些页面优化技巧,比如上面所说的总是把脚本放在页面的底部,压缩合并样式或者脚本文件等,时至今日已不再是最佳的解决方案,甚至事与愿违,转化为性能的毒药。这篇文章所要聊的,便是展示某些不被人关注的浏览器特性或者技巧,来继续完成资源加载性能优化的任务。 一. Preloader 什么是Preloader 首先让我们看一看这样一类资源分布的页面:
复制代码 这类页面的特点是,一个外链脚本置于页面头部,三个外链脚本置于页面的底部,并且是故意跟随在一系列img之后,在Chrome中页面加载的网络请求瀑布图如下: 值得注意的是,虽然脚本放置在图片之后,但加载仍先于图片。为什么会出现这样的情况?为什么故意置后资源能够提前得到加载? 虽然浏览器引擎的实现不同,但原理都十分的近似。不同浏览器的制造厂商们(vendor)非常清楚浏览器的瓶颈在哪(比如network, 同样在资源加载上,早在IE8开始,一种叫做lookahead pre-parser(在Chrome中称为preloader)的机制就已经开始在不同浏览器中兴起。IE8相对于之前IE版本的提升除了将每台host最高并行下载的资源数从2提升至6,并且能够允许并行下载脚本文件之外,最后就是这个lookahead 但我还是没有详述这是一个什么样的机制,不着急,首先看看与IE7的对比: 以上面的页面为例,我们看看IE7下的瀑布图: 底部的脚本并没有提前被加载,并且因为由于单个域名最高并行下载数2的限制,资源总是两个两个很整齐的错开并行下载。 但在IE8下,很明显底部脚本又被提前: 并没有统一的标准规定这套机制应具备何种功能已经如何实现。但你可以大致这么理解:浏览器通常会准备两个页面解析器parser,一个(main 但细节方面却值得注意:
Preloader在响应式设计中的问题 preloader的诞生本是出于一番好意,但好心也有可能办坏事。 filamentgroup有一种著名的响应式设计的图片解决方案Responsive
复制代码 它的工作原理是,当responsive-images.js加载完成时,它会检测当前显示器的尺寸,并且设置一个cookie来标记当前尺寸。同时你需要在服务器端准备一个.htaccess文件,接下来当你请求图片时,.htaccess中的配置会检测随图片请求异同发送的Cookie是被设置成medium还是large,这样也就保证根据显示器的尺寸来加载对于的图片大小。 很明显这个方案成功的前提是,js执行先于发出图片请求。但在Chrome下打开,你会发现执行顺序是这样: responsive-images.js和图片几乎是同一时间发出的请求。结果是第一次打开页面给出的是默认小图,如果你再次刷新页面,因为Cookie才设置成功,服务器返回的是大图。 严格意义上来说在某些浏览器中这不一定是preloader引起的问题,但preloader引起的问题类似:插入脚本的顺序和位置或许是开发者有意而为之的,但preloader的这种“聪明”却可能违背开发者的意图,造成偏差。 如果你觉得上一个例子还不够说明问题的话,最后请考虑使用picture(或者@srcset)元素的情况:
复制代码 在preloader搜寻到该元素并且试图去下载该资源时,它应该怎么办?一个正常的paser应该是在解析该元素时根据当时页面的渲染布局去下载,而当时这类工作不一定已经完成,preloader只是提前找到了该元素。退一步来说,即使不考虑页面渲染的情况,假设preloader在这种情形下会触发一种默认加载策略,那应该是"mobile 二. JS Loader 理想是丰满的,现实是骨感的。出于种种的原因,我们几乎从不直接在页面上插入js脚本,而是使用第三方的加载器,比如seajs或者requirejs。关于使用加载器和模块化开发的优势在这里不再赘述。但我想回到原点,讨论应该如何利用加载器,就从seajs与requirejs的不同聊起。 在开始之前我已经假设你对requirejs与seajs语法已经基本熟悉了,如果还没有,请移步这里:
BTW: 如果你还是习惯在部署上线前把所有js文件合并打包成一个文件,那么seajs和requirejs其实对你来说并无区别。 seajs与requirejs在模块的加载方面是没有差异的,无论是requirejs在定义模块时定义的依赖模块,还是seajs在factory函数中require的依赖模块,在会在加载当前模块时被载入,异步,并且顺序不可控。差异在于factory函数执行的时机。 执行差异 为了增强对比,我们在定义依赖模块的时候,故意让它们的factory函数要执行相当长的时间,比如1秒:
复制代码 为了增强对比,设置了三组进行对照试验,分别是:
复制代码 接下来我们看看代码执行的瀑布图: 1.require.js:在加载完依赖模块之后立即执行了该模块的factory函数 2.sea.js: 下面两张图应该放在一起比较。两处代码都同时加载了依赖模块,但因为没有require的关系,第三张图中没有像第二张图那样执行耗时的factory函数。可见seajs执行的原则正如CMD标准中所述Execution 我想进一步表达的是,无论requirejs和seajs,通常来说大部分的逻辑代码都会放在模块的factory函数中,所以factory函数执行的代价是非常大的。但上图也同样告诉我们模块的define,甚至模块文件的Evaluate代价非常小,与factory函数无关。所以我们是不是应该尽可能的避免执行factory函数,或者等到我们需要的指定功能的时候才执行对应的factory函数?比如: document.body.onclick = function () { require(some_kind_of_module);} 这是非常实际的问题,比如爱奇艺一个视频播放的页面,我们有没有必要在第一屏加载页面的时候就加载登陆注册,或者评论,或者分享功能呢?因为有非常大的可能用户只是来这里看这个视频,直至看完视频它都不会用到登陆注册功能,也不会去分享这个视频等。加载这些功能不仅仅对浏览器是一个负担,还有可能调用后台的接口,这样的性能消耗是非常可观的。 我们可以把这样称之为"懒执行"。虽然seajs并非有意实现如上所说的“懒执行”(它只是在尽可能遵循CommonJS标准靠近)。但“懒执行”确实能够有助于提升一部分性能。 但也有人会对此产生顾虑。 记得玉伯转过的一个帖子:SeaJS与RequireJS最大的区别。我们看看其中反对这么做的人的观点:
首先回答第一个问题。 第一个问题的题设并不完全正确,“依赖”和“执行”的概念比较模糊。编程语言执行通常分为两个阶段,编译(compilation)和运行(runtime)。对于静态语言(比如C/C++)来说,在编译时如果出现错误,那可能之前的编译都视为无效,的确会出现描述中需要回滚或者重新编译的问题。但对于动态语言或者脚本语言,大部分执行都处在运行时阶段或者解释器中:假设我使用Nodejs或者Python写了一段服务器运行脚本,在持续运行了一段时间之后因为某项需求要加载某个(依赖)模块,同时也因为这个模块导致服务端挂了——我认为这时并不存在回滚的问题。在加载依赖模块之前当前的模块的大部分功能已经成功运行了。 再回答第二个问题。 对于“工厂”和“原材料”的比喻不够恰当。难道依赖模块没有加载完毕当前模块就无法工作吗?requirejs的确是这样的,从上面的截图可以看出,依赖模块总是先于当前模块加载和执行完毕。但我们考虑一下基于CommonJS标准的Nodejs的语法,使用require函数加载依赖模块可以在页面的任何位置,可以只是在需要的时候。也就是说当前模块不必在依赖模块加载完毕后才执行。 你可能会问,为什么要拿AMD标准与CommonJS标准比较,而不是CMD标准? 玉伯在CommonJS
其实AMD标准的推出同时也是遵循CommonJS,在requirejs官方文档的COMMONJS
复制代码 CommonJS当然是一个理想的标准,但至少现阶段对浏览器来说还不够友好,所以才会出现AMD与CMD,其实他们都是在做同一件事,就是致力于前端代码更友好的模块化。所以个人认为依赖模块的加载和执行在不同标准下实现不同,可以理解为在用不同的方式在完成同一个目标, 懒加载 其实我们可以走的更远,对于非必须模块不仅仅可以延迟它的执行,甚至可以延迟它的加载。 但问题是我们如何决定一个模块是必须还是非必须呢,最恰当莫过取决于用户使用这个模块的概率有多少。Faceboook早在09年的时候就已经注意到这个问题:Frontend 假设我们需要在页面上加入A、B、C三个功能,意味着我们需要引入A、B、C对应的html片段和样式碎片(暂不考虑js),并且最终把三个功能样式碎片在上线前压缩到同一个文件中。但可能过了相当长时间,我们移除了A功能,但这个时候大概不会有人记得也把关于A功能的样式从上线样式中移除。久而久之冗余的代码会变得越来越多。Facebook引入了一套静态资源管理方案(Static 具体来说是将样式的“声明”(Declaration)和请求(Delivery)请求,并且是否请求一个样式由是否拥有该功能的 当然同时也考虑也会适当的合并样式片段,但这完全是基于使用算法对用户使用模块情况进行分析,挑选出使用频率比较高的模块进行拼合。 这一套系统不仅仅是对样式碎片,对js,对图片sprites的拼合同样有效。 你会不会觉得我上面说的懒加载还是离自己太远了? 但然不是,你去看看现在的人人网个人主页看看 如果你在点击图中标注的“与我相关”、“相册”、“分享”按钮并观察Chrome的Timeline工具,那么都是在点击之后才加载对应的模块 三. Delay Execution 利用浏览器缓存 脚本最致命的不是加载,而是执行。因为何时加载毕竟是可控的,甚至可以是异步的,比如通过调整外链的位置,动态的创建脚本。但一旦脚本加载完成,它就会被立即执行(Evaluate 更加充分的理由是,大部分的页面不是Single Page Application,不需要依靠脚本来初始化页面。服务器返回的页面是立即可用的,可以想象我们初始化脚本的时间都花在用户事件的绑定,页面信息的丰满(用户信息,个性推荐)。Steve Steve Souders的ControlJS是我认为一直被忽视的一个加载器,它与Labjs一样能够控制的脚本的异步加载,甚至(包括行内脚本,但不完美)延迟执行。它延迟执行脚本的思路非常简单:既然只要在页面上插入脚本就会导致脚本的执行,那么在需要执行的时候才把脚本插入进页面。但这样一来脚本的加载也被延迟了?不,我们会通过其他元素来提前加载脚本,比如img或者是object标签,或者是非法的mine Stoyan
复制代码 同时它还列举了其他的一些尝试,但并非对所有的浏览器都有效,比如:
type=prefetch 延迟执行并非仅仅作为当前页面的优化方案,还可以为用户可能打开的页面提前缓存资源,如果你对这两种类型的link元素熟悉的话:
那么上一节延迟执行的方案就可以作为subresource与prefeth的回滚方案。同时还有其他的类型:
|
【让我们再聊聊浏览器资源加载优化】
时间: 2024-11-05 02:37:26
【让我们再聊聊浏览器资源加载优化】的相关文章
让我们再聊聊浏览器资源加载优化
让我们再聊聊浏览器资源加载优化 让我们再聊聊浏览器资源加载优化
[转]让我们再聊聊浏览器资源加载优化
作者 李光毅 发布于 2014年6月27日 几乎每一个前端程序员都知道应该把script标签放在页面底部.关于这个经典的论述可以追溯到Nicholas的 High Performance Javasript 这本书的第一章Loading and Execution中,他之所以建议这么做是因为: Put all <script> tags at the bottom of the page, just inside of the closing </body> tag. This e
也许是被误解的浏览器资源加载优化
几乎每一个前端程序员都知道应该把script标签放在页面底部.关于这个经典的论述可以追溯到Nicholas的 High Performance Javasript 这本书的第一章Loading and Execution中,他之所以建议这么做是因为: Put all <script> tags at the bottom of the page, just inside of the closing </body> tag. This ensures that the page c
网页的资源加载优化
移动开发中很重要的一块是资源的加载优化.移动开发由于网速低带宽,高延迟,移动设备小内存,低处理器性能的原因,因此很多时候不得不通过优化前端页面的性能来满足用户对网页加载的预期. 前段时间做了相关方面的优化,发现网上的中文教程比较少,都是照着chrome开发者网站上一步一步看下来,找问题来解决,因此将部分有用的网页整理翻译了一下. 一.查看网页加载速度 网页加载时长受到网速影响,一般采用浏览器模拟一个特定网速进行测试,这样优化前与优化后的结果会有一个较准确的对比. 方法:打开调试面板-选择网速,一
js资源加载优化
互联网应用或者访问量大的应用,对js的加载优化是不可少的.下面记录几种优化方法 CDN + 浏览器缓存 CDN(content delivery network)内容分发网络, 最传统的优化方式.其实就是将自己页面所依赖的js(静态的)放置到CDN上,或者使用一些CDN库,以此降低对应用服务器的请求,而浏览器缓存也是不重复加载js文件的性质. 优点: 1.简单.容易维护 2.304 cache 简单来说就是转掉请求,缓存不重加载. 缺点: 1.缓存会失效,当用户强制刷新时会有请求 2.无法增量
各浏览器对页面外部资源加载的策略
这个总结来源于一次优化的请求,最初某个页面的加载十分缓慢,load事件迟迟无法触发,因此希望可以通过对静态文件分域名等方式对页面的外部资源进行优化,拿得load事件尽可能早地触发. 于是我查看了页面的源码,并对外部资源进行了整理,基于下面2个理念画出了一个推测的瀑布图: 1.浏览器对同一个域只能并发2个HTTP请求 – 网上盛传已久.2.javascript文件的加载会阻塞浏览器其他资源的加载 – 同样网上盛传已久. 然而,当我看到各浏览器中实际的瀑布图时,我知道自己又犯了一个简单的错误:太过相
chromium kernel资源加载、解析、三树合成浅析(chromium39)
每次尝试去看看chromium kernel中具体逻辑的实现的时候,都会费一些时间去看代码,找逻辑.当然了,网上很多资料可以参看,但是每次看完这些资料,我都会要问一问:确实如此么?新版本的kernel是否这块逻辑更改了呢? 所以,为了让自己释惑,还是要亲自去看源码,一点点查看调用堆栈.然后再能在整体上理解下kernel的原理. 最近了看了看chromium kernel中,blink kernel part的网页的加载.html解析.以及三树(Dom tree. Render Tree.Rend
【原创】我所理解的资源加载方式
最近转战unity3d,接的第一个任务就是关于资源管理的部分. 由于项目是web和standalone(微端)并存的,所以希望保证业务逻辑尽量保持一致,跟之前ios,android的执行流程略有不同,比如在web模式下,FILE类是被禁用的,所以指望通过写文件来操作相关功能参数的方法是不可行的.下面,是我对这方面的一些理解.本文中如果没有特殊说明,下载的资源都是AssetBundle web程序的运行流程大致是 首先加载web.html,这是整个程序的入口 他会加载相关的unity3d文件(本例
资源加载和页面事件 load, ready, DOMContentLoaded等
资源加载和页面事件 理想的页面加载方式 解析HTML结构. 加载并解析外部脚本. DOM树构建完成,执行脚本.//DOMInteractive –> DOMContentLoaded 加载图片.样式表文件等外部文件. 页面加载完毕.//window.onload 涉及到的事件 window.onload: 当页面全部加载完成(包括所有资源) document.onload: 当整个html文档加载的时候就触发了,也就是在body元素加载之前就开始执行了 DOMContentLoaded: 当页面