网站为什么 JS 调用尽量放到网页底部?

作者:igetit
链接:https://www.zhihu.com/question/34147508/answer/63068656
来源:知乎
著作权归作者所有,转载请联系作者获得授权。

这是个Meta级别的好问题!如果你想把web前端性能优化到极致,一定要认真地去了解这个原则背后的原理,而非表面的技巧。

(已完结,转载请署名,否则保留追究的权利)

事实上,如果对web优化比较了解,只要一句话就能说清楚了。 web页面性能优化其精髓就是——将浏览器基本无序的资源加载请求用js有序地控制起来,包括js本身。

这个原则几乎适用于所有web场景,只是它演变出来的具体做法千差万别,PC端和H5端由于环境不一样,也要有不同的玩法,而Bigpipe也是基于这个理念。但请问你能理解吗?

如果你能理解,那么可以不往下看,如果不能理解,建议看看,或许有收获的。 我主要以PC端几个知名电商网站的优化为案例,来说明将js放在html不同位置都有什么不同。

-----------
一、为什么我会认为这是个好问题?!

在我带过的几个电商项目前端团队中,就因为这个问题开过几个不开窍的前端。

理由很简单,也比较霸道。这几天我们项目又在做前端性能优化,又有同事拿着这个疑惑来问我,呵呵,不会再乱开人了。

以前为此开人,可能是我没能力用通俗易懂的文字来描述好这个问题,难免有误伤。为了避免这样的事情,我决定要好好把问题说清楚,就从浏览器渲染的原则开始!

二、是不是网页JS调用都尽量放到网页底部?

按知乎的原则,先要问是不是,再问为什么,但这个'是不是'对于做前端技术的人来说,一眼就能看出个所以然。

显然,不是的!但性能优化做得好的网站基本上是这个原则。给两个我认为前端优化做得比较好的知名海淘类电商,大家去观摩一下别人的做法:

-----------
网易考拉海购!
贝贝网
-----------

当然,我们不能用Bigpipe这种极端的优化技术流派来解答,或许是解答不了的,但即便是bigpipe其背后的原理也是和这个原则并没有冲突的,大道至简,底层的原理是一样的,只是它尽全力地利用每一次http请求,用前端的技术手段来加载足够多的资源。

当然了,题主应该是一个前端开发,而且应该是在碰到页面优化需求或学习上的疑惑了,可能有人强逼这TA按照这样的原则去做,但又不知道什么别人为啥要求他这么做,于是就有了这个问题!

至于情况是不是这样的,我只是瞎猜的。但是,我带的前端团队里面有不少同学存在同样的疑惑,因为我有这样一个规范:
<img src="https://pic1.zhimg.com/5c60a0428572a3264793df3819ba0f28_b.png" data-rawwidth="757" data-rawheight="753" class="origin_image zh-lightbox-thumb" width="757" data-original="https://pic1.zhimg.com/5c60a0428572a3264793df3819ba0f28_r.png">不知道有多少团队有这样的标准化demo模板规范?这是我1年多以前做的了,在这里—— 不知道有多少团队有这样的标准化demo模板规范?这是我1年多以前做的了,在这里—— 基于gulp的前端框架开发规范。大家可以去我的博客看看,权当参考。当然,现在这个规范已经有了新版本。

--------------------
三、为什么网页JS调用都尽量放到网页底部?

这部分如果《高性能网站建设指南》这本书上有详细说明的并且我也认同的,就直接截图贴过来,这里我只说自己的理解部分。还有,对于书本上的东西,我的态度是——尽信书,不如无书。

(一)大家的做法是不是一样的?

ok,我先把问题分解一下。

首先这里有个关键词——尽量。《高性能网站建设指南》这本书里面用的词是“如果可以的话”。
<img src="https://pic2.zhimg.com/2a9ff662555a6a963cb71fb98e36ca69_b.jpg" data-rawwidth="711" data-rawheight="337" class="origin_image zh-lightbox-thumb" width="711" data-original="https://pic2.zhimg.com/2a9ff662555a6a963cb71fb98e36ca69_r.jpg">
也就是说,js不完全是一定要放在页面底部的,但是你要了解清楚以下两个问题了:
  1. 什么是尽量的(可以的)那部分js代码?
  2. 什么是不尽量的(不可以的)那部分呢?
只有了解清楚这两个问题,你才知道如何去安排js在页面中的位置。ok,我们来看看考拉网和贝贝网的首页源代码。
  1. 考拉网:<head></head >之间就一段让IE9以下浏览器兼容HTML5标签的js代码,这是一个底层的兼容脚本,不涉及任何页面逻辑,而它的全部页面逻辑都是放置在脚步。
  2. 贝贝网:<head></head >之间放置的是一些全局设置和一些统计脚本,也不涉及页面逻辑,逻辑部分js也是放在页面底部。
  3. 我们项目的:做法是两者的结合,heah标签内就的js脚本就只是定义几个全局的命名空间和一段统计脚本,没了,而业务逻辑js就放置页面最底部。
&amp;lt;img src=&quot;https://pic2.zhimg.com/a337d5d4b8164a7befaf84af8b56bc15_b.jpg&quot; data-rawwidth=&quot;789&quot; data-rawheight=&quot;500&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;789&quot; data-original=&quot;https://pic2.zhimg.com/a337d5d4b8164a7befaf84af8b56bc15_r.jpg&quot;&amp;gt;
整体对比来看,css样式规划大家都基本相同,是1个全局+1个当前,文件名上通过md5戳来解决强缓存问题,js的缓存解决方案也是一样的做法。
&amp;lt;img src=&quot;https://pic2.zhimg.com/805ed2242c25a6ad04a7597924285f25_b.png&quot; data-rawwidth=&quot;486&quot; data-rawheight=&quot;318&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;486&quot; data-original=&quot;https://pic2.zhimg.com/805ed2242c25a6ad04a7597924285f25_r.png&quot;&amp;gt;
这是我们的PC端js的大致布局。考拉网也是类似分配方式,只是把core和common合并在一起(超过了200K),我们没有。我是觉得合并在一起这样模块太大了,不太利于弱网用户,但多一次http请求,有利有弊吧。
&amp;lt;img src=&quot;https://pic3.zhimg.com/9d8964c97f9324d4595ff4b7228507b2_b.jpg&quot; data-rawwidth=&quot;953&quot; data-rawheight=&quot;646&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;953&quot; data-original=&quot;https://pic3.zhimg.com/9d8964c97f9324d4595ff4b7228507b2_r.jpg&quot;&amp;gt;
有部分人可能会问,为啥不统统合并在一起,就1个HTTP请求了?我只能呵呵,网站不是只有1个首页,还有很多其他页面呢,只要把js底层库和常用的公共类库加载一次,其他页面就可以被缓存起来(form cache或304)

这个前端技术表面的对比,是不是有点意思呢?

我们几个都是海淘类垂直电商,只是定位和强势品类有些小差别,但在业务层面其实是基本相同的,都是海淘。也就是说,我们互为竞争对手!那么,很显然我们不可能相互沟通、开会,然后通报我用什么前端架构,前后端协作开发的模式等等技术问题,但是为什么做法上大家是那么的一致呢?

这就是问题了!我绝对保证不认识考拉的前端架构设计师,但我的前端设计方案出来的结果几乎是一模一样的!为什么呢?

简单点说,条条道路通罗马。就这样,没为什么。后面我汇以网易考拉为案例,逆向分析他们的做法,进而尝试窥探他们的前端架构设计方案。不一定正确,只是个人看法。

如果你想得到绝对正确的答案,就想办法进入里面,或者发一个类似这样的问题( 美团的前端架构是怎样的? - 前端开发),看看有没有人出来回复。

-------2015年09月10日22:56:43---------
这里上一个简单的web优化对比图(阿里测,专业的网站即时探测工具):

&amp;lt;img src=&quot;https://pic2.zhimg.com/96f3eb9fceb9be6a6d8250006e4f6525_b.jpg&quot; data-rawwidth=&quot;1024&quot; data-rawheight=&quot;1698&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;1024&quot; data-original=&quot;https://pic2.zhimg.com/96f3eb9fceb9be6a6d8250006e4f6525_r.jpg&quot;&amp;gt;对比只是一个参考,不见得我们的技术开发人员实力很好,很多就是一般水平的。而且电商的竞争很多时候不仅仅是技术力量的角力,还有产品理念,运营能力,市场推广等等综合因素决定的,技术只是基础,特别是后进场的玩家,我们的项目上线才4个多月,我不想透露太多,避免中枪。 对比只是一个参考,不见得我们的技术开发人员实力很好,很多就是一般水平的。而且电商的竞争很多时候不仅仅是技术力量的角力,还有产品理念,运营能力,市场推广等等综合因素决定的,技术只是基础,特别是后进场的玩家,我们的项目上线才4个多月,我不想透露太多,避免中枪。

这里是显摆的,我们项目最近优化的结果:
&amp;lt;img src=&quot;https://pic2.zhimg.com/c11a932ee11a5792f5319c4d6b421fd9_b.png&quot; data-rawwidth=&quot;1029&quot; data-rawheight=&quot;508&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;1029&quot; data-original=&quot;https://pic2.zhimg.com/c11a932ee11a5792f5319c4d6b421fd9_r.png&quot;&amp;gt; 这次的优化心得:
由于我们运营增加了2个第三方统计,是的我们对第三方静态内容缓存控制力度下降了,但是我们改善了gzip和图片的压缩优化,比前几天提高了几分,但我认为还有优化空间,比如:
  • css用到的雪碧图的压缩比例还不够,需要改进前端构建框架,改进图片压缩的算法,下个星期上一个行版本,看看效果
  • html文档并没有迷你化,要将服务端的模板弄到前端构建流程里面,控制起来做压缩,这个有利有弊,利就是可以在发布到服务端前作压缩,弊端就是迷你化后不太方便调试
这两点弄好了,上90分应该不成问题。如果也是做这一块的,可以将经验分享出来,相互学习。我们和考拉网的代码量几乎一致,交互也基本相同,有一定对比价值。

----------
这里必须补充一点:
用类似阿里测这种工具来对比,只能作为一种参考,如果大家的业务不同,页面的Dom数量差异太多,且交互场景也有很大差别,那么这种对比是没有任何意义的。

----------

(二)js在页面中不同位置带来的影响或效果区别

(这里将是讲浏览器资源加载原理和js执行原理的,用通俗易懂的方式说明白需要死掉很多脑细胞,查阅很多很多资料,可能包括已经还给老师的E文,会慢一点点。我争取说得通俗易懂,但发现很难,大家要有心理准备。)

要彻底搞懂,为什么别人建议js放在页面的底部,那么我需要从js的语言机制及其运行环境说起。

1,浏览器不是单线程的,它多线程的,如果有必要它还是多进程的。

很多同学并不理解浏览器不是单线程的,别问我为什么,暂时不打算做代课老师,这里有两篇,自己去理解。

浏览器是怎样工作的(一):基础知识
浏览器是怎样工作的:渲染引擎,HTML解析(连载二)

例如,Webkit或是Gecko引擎,都可能有如下线程:
  • javascript引擎线程
  • 界面渲染线程
  • 浏览器事件触发线程
  • Http请求线程

2,js是单线程的


证明的脚本:
&amp;lt;img src=&quot;https://pic1.zhimg.com/d4668c35dba4e13e5e26772326b1efb0_b.jpg&quot; data-rawwidth=&quot;576&quot; data-rawheight=&quot;319&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;576&quot; data-original=&quot;https://pic1.zhimg.com/d4668c35dba4e13e5e26772326b1efb0_r.jpg&quot;&amp;gt;
在1万次循环迭代过程中,foo()一直先打印了1万次‘first’,定时时间执行时间为0,它也不去执行里面的mylog函数,而是等待循环结束后,再输出1万次‘second’,看起来像运行了两次迭代,是不是表现很怪异?

为什么?

因为 JS运行在浏览器中,是单线程的,每个浏览器页面就是一个JS线程,既然是单线程的,在某个特定的时刻只有特定的代码能够被执行,并阻塞其它的代码。而浏览器是多线程的,它又一个名叫 Event driven(事件驱动)的线程,而且浏览器具备Asynchronized(异步)执行事件的特性,会创建事件并放入执行队列中,异步执行。

浏览器定义的异步事件有很多种,例如mouse click(鼠标点击事件), a timer firing(定时器触发事件), 或者an XMLHttpRequest completing(XMLHttpRequest完成回调事件),一旦js代码中有这样的事件代码,浏览器就会将它们放入执行队列,等待当前js代码执行完成之后,再按队列情况逐个执行。

于是,我们就看到了上面的 setTimeout(mylog, 0); 这段代码被执行的怪异表现了,也就是mylog的执行顺序被改变了。
一个有用的知识点: setTimeout(func, 0)的作用
  • 让浏览器渲染当前的变化(很多浏览器UI render和js执行是放在一个线程中,线程阻塞会导致界面无法更新渲染)
  • 重新评估”script is running too long”警告
  • 改变代码块的执行顺序
3,浏览器对资源的加载是线性的,可并行的,但js除外

当我们在浏览器的地址栏里输入一个url地址,访问一个新页面时候,页面展示的快慢就是由一个单线程所控制,这个线程叫做UI线程,UI线程会根据页面里资源(资源是html文件、图片、css等)书写的先后顺序,它会按照资源的类型发起http请求来获取资源,当http请求处理完毕也就意味着资源加载结束。

但是碰到javascript文件则不同,它的加载过程被分为两步,第一步和加载css文件和图片一样,就是执行一个http请求下载外部的js文件,但是javascript完成http操作后并不意味操作完毕,UI线程就会通知javascript引擎线程来执行它,如果javascript代码执行时间过长,那么用户就会明显感觉到页面的延迟。

为什么浏览器不能把javascript代码的加载过程拆分为下载和执行两个并行的过程,这样就可以充分利用时间完成http请求,这样不是就能提升页面的加载效率了吗?

答案当然是否定的。

因为javascript是一个 图灵完备的编程语言,js代码是有智力的,它除了可以完成逻辑性的工作,还可以通过操作页面元素来改变页面的UI渲染,如果我们忽略javascript对网页UI界面渲染的影响,让它下载和运行是分开的(也可以理解为js代码可以延迟执行),结果会造成页面展示的混乱,或多次重绘。很显然,这样的做法是不合适的,因此,js脚本的下载和执行必须是一个完整的操作,是不能被割裂的。
&amp;lt;img src=&quot;https://pic1.zhimg.com/c89638665187ba132e58603eb21cb540_b.jpg&quot; data-rawwidth=&quot;1436&quot; data-rawheight=&quot;619&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;1436&quot; data-original=&quot;https://pic1.zhimg.com/c89638665187ba132e58603eb21cb540_r.jpg&quot;&amp;gt;百度首页的资源下载瀑布图(没有任何缓存状态下,所有http请求响应状态都是200) 百度首页的资源下载瀑布图(没有任何缓存状态下,所有http请求响应状态都是200)

既然拆分js的下载和执行是不可行的,但为了提升用户体验,加快UI线程的执行又是一个无法回避的问题,于是浏览器就换了种方式,让它在同一个时间可以下载多个资源。

例如上面百度的截图,在同一个域名下,firefox可以同时下载两种图片(chrome可以同时下载4个静态资源),不过这是针对图片和css文件,对于js文件似乎还是一个接着一个的下载,下载一个执行一个,不过到了js执行时候还是要严格按照顺序执行。当然,我在途中用黄色标记的几个js是并行加载的,其实是先前某个js发起的请求(这种做法就是无阻塞的js加载,也叫异步加载,后面我会说明这个东西有什么好坏)。

多个http连接并行下载资源就好比多个线程共同完成某个任务,如果并行http连接更多,那么能有更多http资源同时被下载,但是浏览器提供并行执行的http连接实在太少了,例如上面firefox才两个,chrome也只有4个,那如何突破浏览器的连接个数的限制了?

方法很简单就是将常用的,稳定的静态资源统一放在静态资源服务器上,由统一的域名对外提供连接,而这个域名要和主域名不一样就可以了。也就是将静态资源放在CDN节点上,单独用一个域名来对应。

到这里,可能有人会问,是不是给每个静态资源分配一个域名,让所有资源都可以并行下载就会达到最佳状态了呢?

答案当然也是否定的。简单滴说,有两个方面原因:

一方面,我们采用的是http1.1版协议,它的特点是资源下载过程是一个长连接,而长连接的好处是在页面和服务端频繁交互时效率更好,但http协议有时候不是那么的可靠,导致服务器要维护一些无用的长连接,访问的人次越多越糟糕。另一方面,当我们同时在一个页面中使用的域名过多,会导致dns解析的开销增大。

那么,多少个域是最合适的呢?好像是雅虎军规中有提过,最佳的建议是2个。也就是图片一个CDN域,css和js一个域。

4,放到网页顶部的js就一定阻塞页面渲染吗?

刚才说了,js之所以会阻塞UI线程的执行,是因为javascript能影响甚至控制UI渲染的过程,而页面加载的规则是要顺序执行,所以在碰到js代码时候UI线程就会通知js引擎来执行它。

然而,很早很早以前,很多程序员不知道这个特点或者知道但被忽视,因此导致编写代码时候将用于展示的代码和用于处理逻辑的代码混淆在一起,这样做的后果是使js代码造成的阻塞更加严重,于是业界良心的雅虎出台了这个军规——将js脚本放置到html文档的末尾。

如果不想深究这个话题,其实到这里就可以结束了。但是,我个人不是很喜欢按常规套路出牌,比如对于军规进行一个反问——难道将js脚本放置在head部分就一定会阻塞页面渲染吗?

答案其实依然是否定的。我简单用常用的jQuery.lazyload插件整了两个简单的测试

sandbox.runjs.cn/show/m -->这个是所有js在头部的
sandbox.runjs.cn/show/b -->这个是所有js在尾部的
(这里请对比源代码)
两者的效果是一致的,但如果我们把js头部的lazyload实例化的脚本改成不放在 $(document).ready() 这个方法里,而是直接实例,那么lazyload就失效了。
//原来的
$(document).ready(function() {
    $("img.lazy").lazyload();
});
//如果改成这样,并放在head标签内部,那么lazyload就失效
//但是如果我们将这个放在所有的img元素以后,那么lazyload就又生效了
$("img.lazy").lazyload();

为什么会这样?

虽然我们将js全部放在头部,但事实上是利用jq的一个延迟执行的接口——$(document).ready() ,让js逻辑( $("img.lazy").lazyload(); )的执行延迟到文档准备好了之后,因而保障了lazyload在执行的时候,页面中的img标签是存在的。

但是,如果不放在这个接口里面,那么头部的js逻辑就会在文档准备完成之前执行,这时候页面中还没有img元素,因此也就失效了。

不过,如果我们将这段实例化的逻辑弄到后面(所有img标签之后),即便不放在$(document).ready里面执行,lazyload就又能生效了。

但这个时候,其实这段逻辑依赖的jQuery库和lazyload插件并不需要放在head里面了,而只要保持在实例化的逻辑之前一点点就可以了,而这种做法就是雅虎军规推荐的。

当然,这里的结论是 “放到网页顶部的js不一定阻塞页面渲染”,只要将实例化的js接口或方法封装在$(document).ready接口内,这样就可以保障逻辑能够顺利进行。
也可以这么写:
$(function(){
    $("img.lazy").lazyload();
})

5,为什么电商网站喜欢将js逻辑放在脚部呢?

很显然,这并不是海淘电商的前端架构设计者有特殊的癖好,而是业务优化的需要。但是,这个需要结合不同的架构情况来具体分析,如果涉及到需要用前端模板引擎渲染页面的,情况就更加复杂了。

比如 唯品会,大家先去观摩一下她的首页,是不是有很多类似这样的代码:
<!-- 导航选择分区浮层 -->
<script type="text/html" id="J_selectArea_list">
<div class="sel-area-box-inner" id="J-areaBinner">
  <i class="ico-arw"></i>
  <p class="sab-tit">请选择所在的收货地区</p>
  <table class="sab-table" mars_sead="home_top_zone_link">
    {#items}
    <tr>
      <th>{$sort}</th>
      <td>
        {#item}
        <span class="J_select_item" mars_sead="te_home_head_diqu_link" data-wh="{$warehouse}" data-id="{$id}">{$name}</span>
        {#/item}
      </td>
    </tr>
    {#/items}
  </table>
</div>
</script>
<!-- 导航选择分区浮层 end -->

这就是前端模板引擎的标志性代码。那么,就这种情况,我先提出几个疑问:

a,请问当页面有大量这样的代码需要处理和维护时,页面中的js到底怎么布置才是合理的呢?
b,你不觉得,这样的模板维护起来是不太容易吗?直接放置在页面上,如果这个页面要经常变动(首页显然是变动频率比较高的),前端后端都可能有人来维护这份代码,请问冲突了怎么解决呢?有没有办法将冲突风险降到最低?
c,请问如果我们不把这些文件直接放在页面上,比如将它们弄到在某个js里面,这样做可以吗?
d,如果要这样做,如何让前端开发人员容易维护这样的代码,比如保存即可看到效果,需要怎样一种前端架构设计来完成这需求呢?

严重声明:
1. 这里并不是想拿VIP开刷,而是我真的一时半会找不到更合适、更有代表性的对象了。这些问题涉及前端开发模式选择的问题,这个章节我不打算展开,留给后面的前端架构分析来补充!
2. 虽然我觉得VIP的做法还有更好的选择,但并不是说换我去实施就一定比现在负责这一块的同行做得更好,真心不是这样的。电商这一个领域,很多时候业务的实现都是有时限的,特别是VIP这种高速发展的电商,因此这里只是一种局外人的视角,随便说说罢了。

先解决这个问题——
为什么我会说电商喜欢将js放在页面的尾部是因为业务优化的需要呢?

至少我已经找到了3家(就是前面对比的),因此这不是个例。这里要搞清楚“电商的业务优化需要”是什么?

首先,我们要知道,电商其实就是和钱打交道的网站,卖东西,然后收钱,本质上和你我他家门口的小卖部没啥两样,只是我们通过网络来进行,而网络这种东西是不透明的,你不知道卖东西的是不是一条狗还是一只猫,当然买东西的一样,大家相互信任的基础是非常薄弱的。

好在淘宝等一大批先驱将网络支付的这种文化或习惯培养了起来,我们后进场的电商玩家就要基于各种在线支付手段来完成买卖,但是玩家人数很多啊,而且都不是BAT级别的,本来用户对于这种电商的信任度就比较低,大家对于任何可以提高用户体验的细节都要做到斤斤计较,能有多完美就要多完美。

还有很多,但必须打住...我只是为了照顾部分客官,打了这么多废话出来,其实就是想说明电商的竞争非常激烈,极致的用户体验是分出胜负手的关键,这就是前端技术发挥余力的地方了。比如,尽快输出首屏幕的页面内容,尽快地让用户可以进行交互,等等吧。首页首屏秒开已经是电商的最低要求了,秒开,你懂吧?

那么,电商的首页首屏都有啥内容呢?如何以极限的速度呈现给用户呢?

咱们是前端,我只能限制在前端的范畴。假设html文档都是150Kb左右,gzip之后是30Kb左右,网络带宽一样,而用户获得文档的时间基本是一致的。在这些前提下,我们同时获得文档之后,如何根据html结构布置js及其交互逻辑,才能以最快速度呈现首屏内容并提供交互呢?

这里就以考拉网为例,其首屏内容如下图:
&amp;lt;img src=&quot;https://pic4.zhimg.com/d00c43c0251a039d3828e96a60367ccf_b.png&quot; data-rawwidth=&quot;1440&quot; data-rawheight=&quot;900&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;1440&quot; data-original=&quot;https://pic4.zhimg.com/d00c43c0251a039d3828e96a60367ccf_r.png&quot;&amp;gt;
其首屏内容可细分为
  1. header部分(包括topbar、logo、搜索框和导航)
  2. slider广告幻灯片(多图)
  3. 四个优势提醒(网易自营,低价保障,闪电发货,全场包邮)
  4. 一个通栏广告(就是App下载的那个)
由于浏览器线性加载的特点,首屏内容看到之前,我们至少会得到文档内容和css样式,如果我们第一时间让用户看到页面,那么,这里就应该只有首屏的图片就可以了,比如logo、购物侧图标、banner大图,四个优势对应的图标以及广告图片。

如果我们将js布置在head里面,那么碰到js就要第一时间去加载js,这里就要消耗浏览器加载资源,对吗?而且js是阻塞方式加载的,即便是加载足够快,但也阻挡了首屏的内容!也就是,将js放在这里是不合适的。

但是,我们将js放到文档的后面,如果前面有很多很多的图片资源,那么浏览器发起js加载请求可能就要等这些图片加载之后,那么首屏的页面却需要js提供交互了,比如幻灯片就是这种情况,那么首屏之后的内容就不能干扰js的加载,否则用户体验就很差,因为发生了TTI延迟问题(这个TTI兄弟,它有很多可以诉说的故事,切莫着急,俺会慢慢道来滴)。

事实上,网易考拉网的首页首屏内容的呈现的优化空间,至少有两点:

第1点:

&amp;lt;img src=&quot;https://pic1.zhimg.com/5d3ee0961fca6fb6c18dec3c9d2a5f1c_b.jpg&quot; data-rawwidth=&quot;1125&quot; data-rawheight=&quot;475&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;1125&quot; data-original=&quot;https://pic1.zhimg.com/5d3ee0961fca6fb6c18dec3c9d2a5f1c_r.jpg&quot;&amp;gt;导航里面有很多“热卖大牌”,带有logo的,这些logo图片是直接用src发请求的,这是完全没有必要的,因为第一次进来用户没有触发导航下拉之前看不到这些东西,或者他可能从来到这里到离开,都不会点击打开下拉列表。 导航里面有很多“热卖大牌”,带有logo的,这些logo图片是直接用src发请求的,这是完全没有必要的,因为第一次进来用户没有触发导航下拉之前看不到这些东西,或者他可能从来到这里到离开,都不会点击打开下拉列表。

比如我就是一个案例,在没有写这个文章之前,也不知道导航里面有那么多图片的,研究了源代码才发现这里还有那么多精彩,因此建议用js控制起来,用户没有触发之前就不要发起请求了。

第2点:
&amp;lt;img src=&quot;https://pic3.zhimg.com/b3c1e9abc5c4da5fb33183c3240d5dce_b.jpg&quot; data-rawwidth=&quot;1245&quot; data-rawheight=&quot;560&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;1245&quot; data-original=&quot;https://pic3.zhimg.com/b3c1e9abc5c4da5fb33183c3240d5dce_r.jpg&quot;&amp;gt;首屏内容中轮播大图在用户进来之后就全部加载了,这也是没有必要的,只需要其中的一张即可。也就是,进入页面后用img的scr发起一个下载请求,其他img不要即刻去发起下载,而是等到js加载之后,用js来发起其他图片请求,等待所有轮播图片都加载完成之后,再执行轮播的逻辑。 首屏内容中轮播大图在用户进来之后就全部加载了,这也是没有必要的,只需要其中的一张即可。也就是,进入页面后用img的scr发起一个下载请求,其他img不要即刻去发起下载,而是等到js加载之后,用js来发起其他图片请求,等待所有轮播图片都加载完成之后,再执行轮播的逻辑。

但是,如果要用js控制轮播图的加载就会有一个技术上的难点,如何知道轮播图片加载完成了呢?是全部完成之后再提供轮播切换交互,还是出现加载完一张就多一个切换,抑或是当加载到第n张之后就开始提供可交互,而不管n张以后的是否加载完成呢?此外,如果是要兼容IE6/7/8,坑很多。

按照这个思路,在兼顾SEO的前提下,PC端电商首页首屏要以极限的速度呈现给用户,我的做法是:
  • 也就是说,电商网站PC端做极限的性能优化,其精髓就是——将浏览器基本无序的资源加载请求有序地控制起来,包括js本身,这就是js要放在脚步的缘由。
  • 除了首屏看得见的资源(主要是图片资源)外,其他资源一律需要通过js来控制,而不能随意地发起http请求(包括首屏看不到的资源)。按照这个原则,js只能放在body标签闭合之前,并且js逻辑不能随意书写!
其实这样做是有代价的,图片资源无法被搜索引擎正常抓取!但是这个对于电商来说,重要吗?大家都是要通过购买流量来PK的,所以不太重要。不同版本的浏览器下,对于资源下载都有一定优化,webkit内核的浏览器表现好很多,其他浏览器尤其是老版本的IE表现有时候让人搞不明。

以考拉网为案例,说明电商网站喜欢将js放在底部,其缘由其实是因为业务优化需要,但并不是每一家都会这么做,典型的代表例如Tmall、VIP等,为什么有还是有不少电商是不放在底部的呢?

其实很多时候还是要看业务需要,以及和一开始的技术选型有关,这个真不是一时半会可以说得清楚的,这里就不展开了。

-----编辑于2015-09-13 09:12:23-----
(三)逆向分析考拉网的前端架构方案

这里分享的是根据别人将js放在底部,并且只能看到一大堆压缩优化后的代码,如何去学习别人前端架构设计的一种方法或途径。

当然,我经常这么做,收获颇大,有时候想偷窥别人的源代码,这样才是最接近真实情况的,有时候确实也能瞎猫撞上死耗子。不管怎样,我觉得这种方法还是可行的,分享给大家。

还是以考拉网为例。我先把首页html框架抽出来,梳理出大致页面结构,然后结合js,来看看别人是怎么做的。考拉的首页html框架如下:
<!DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8" />
    <title>网易考拉海购</title>
    <link rel="stylesheet" href="http://mm.bst.126.net/build/combo_f0ae46c.css?v=201509122013" />
    <link rel="stylesheet" href="http://mm.bst.126.net/build/combo_935bd74.css?v=201509122013" />
</head>

<body class="indexPage">
    <nav id="topNav"></nav>
    <h1 class="hide">网易考拉海购</h1>
    <header id="docHead"></header>
    <nav id="topTabBox"></nav>
    <div class="wrap">
        <article class="topBgWrap clearfix">
            <!-- sliderBox -->
        </article>
        <article class="m-slogan">
            <!-- 四个优势列表 -->
        </article>
        <div class="mainBgWrap clearfix">
            <article class="m-recomds">
                <!-- 每日上新列表 -->
            </article>
            <article class="m-halfbanner">
                <!-- 四个半屏banner列表 -->
            </article>
            <h2 class="w-tit1"><span class="big">今日限时特卖</span></h2>
            <div id="hotsaleblock">
                <!-- 今日限时特卖列表 -->
            </div>
            <article class="m-recomds m-recomds-next">
                <h2 class="w-tit2"><span class="big">下期特卖预告</span></h2>
                <!-- 下期特卖预告列表 -->
            </article>
            <h2 class="w-tit1"><span class="big">全球精选</span></h2>
            <div class="m-hslist clearfix">
                <!-- 全球精选列表 -->
            </div>
            <article class="m-mustbuy">
                <h2 class="w-tit1"><span class="big"> 海淘必买 / </span></h2>
                <!-- 海淘必买列表 -->
            </article>
        </div>
    </div>
    <div id="rightBar" class="m-rightbar">
        <!-- 左侧导航 -->
        <!-- 这里语义有点问题,应该是右侧那条黑色Bar -->
    </div>
    <footer id="docFoot">
        <!-- footer导航、版权区域 -->
    </footer>
    <script src="http://mm.bst.126.net/build/combo_a235696.js?v=201509122013"></script>
    <script>
    //对导航效果进行初始化
    //这里为啥不判断 Core 是否存在呢?还不够严谨。
    Core.navInit("http://mm.bst.126.net/", "", "201509122013", 1442105236646, true)
    </script>
    <script src="http://mm.bst.126.net/build/combo_ef4c658.js?v=201509122013"></script>
    <script>
    //对导航效果进行初始化
    //这里的判断严谨多了
    //连续并列运算可简化判断,而且效率更高,请学习!
    Core && Core.quickInit && Core.quickInit();
    </script>
    <script>
    //code here,省略
    </script>
    <script src="http://kxlogo.knet.cn/seallogo.dll?sn=e15040881010058239iy2f000000&size=0"></script>
    <script src="http://analytics.163.com/ntes.js" type="text/javascript"></script>
    <script type="text/javascript">
    _ntes_nacc = "kaola"; //声明要统计的域
    neteaseTracker(); //网易自家的统计跟踪
    neteaseClickStat(); //网易自家对用户行文跟踪
    </script>
    <script type="text/javascript">
    //code here
    //这里还是一大堆第三方统计
    </script>
</body>
</html>
这份html结构可以膜拜一下,它在语义化方面做得非常出彩,几乎完美,特别是对H标签的使用非常合理,出神入化了,功力非常深厚,这种做法和163首页的做法如出一辙。可想而知,这一首页的html开发者应该系出同门,同样也可以想到网易内部的技术培训应该是完善的,建议大家好好观摩,值得学习。

好了,神拜完了。我们开始吧。 第一个问题,网易考拉的js底层框架或类库是什么?

咱们就从它的第一个js文件找找起,这就应该是他们的最基础的框架或类库,就这个 mm.bst.126.net/build/co

大家不妨点击打开,然后格式化看看,工具比如 在线代码格式化,或者在Sublime text中安装一个格式化的插件,我用的是 HTML-CSS-JS Prettify ,这个比较好用,其他编辑器没研究。格式化完成后,很容易找到 jquery: "1.4.2" 。就是这个了,底层是jquery-1.4.2版本,和官方的 code.jquery.com/jquery-对比一下,没有改动。

继续分解压缩后的代码,如下(跪求源码):
//jquery 1.4.2
!function(e, t) {
    e.jQuery = e.$ = g
}(window);

//这里是一个二维码生成器基类,全局的
//学习点,QRCode定义在全局,但其实则放在一个闭包内部,避免污染
var QRCode;
!function() {
    QRCode = function(t, e) {
        //code
    }, QRCode.prototype.makeCode = function(t) {
        //code
    }, QRCode.prototype.makeImage = function() {
        //code
    }, QRCode.prototype.clear = function() {
        //code
    }, QRCode.CorrectLevel = l
}();

//这里又是一个闭包,闭包内部主要是用来扩展jQuery的方法
//例如扩展Number/String原型方法、判断IE678、cookie等等
!function(e, t, i) {
    //一大堆code
    //这里有个有意思的东西,应该是一个弹窗
    //不过是用Core.loadCdnJS异步加载的
    t.dialog = t.dialog || function() {
        var e = arguments;
        Core.loadCdnJS("js/dialog.js", function() {
            t.dialog.apply(t, e)
        })
    }
}(window, jQuery);

//这里就是页面上看到的自定义基类
var CopyCore = Core;
var Core = function(e, t, i) {
    //一大堆code
}(window, jQuery);
jQuery(window).unload(function() {
    //code
});
//(⊙o⊙)…这里是让自定义基类Core在文档准备完成后初始化
jQuery(document).ready(function() {
    Core.init()
});
//按语义,easyNav应该是一个快速导航的方法
var easyNav = function(e, t, i, n) {
    //其实不用纠结是干什么的
};
//这里又是一个自运行的闭包
!function(e, t, i, n, o) {
    //反正就是一大堆业务逻辑相关的代码
    //其实不用纠结是干什么的
}(window, jQuery, Core, easyNav);

//这里又是一个自运行的闭包
!function() {
    //这里里面好像是一些修复js原生方法缺陷的代码,比如json的处理
    //其实不用纠结是干什么的
}();
//这里定义了一个全局对象,带两个属性
this._nisas = {
    _$host: location.host,
    _$doc: document
};

//这里又是一个自运行的闭包
!function(e) {
    //密密麻麻的不知道干什么,猜测是用来加密的,看得头晕眼花的
}(this._nisas);

//这里也还是一个自运行的闭包
!function(e, t, i) {
    //autoSearch,也就是搜索时,输入关键词后自动联想的方法
}(window, jQuery);

//这里依然是一个自运行的闭包
!function(e, i, n) {
    //目测就是一个弹出层登录注册业务空间
}(window, jQuery, easyNav);

----编辑于2015-09-13 12:54:22,继续偷窥----
这个文件 mm.bst.126.net/build/co,内容就比较简单了,脉络如下:
! function(t, e, o, i, n) {
    //一大堆业务逻辑,目测是全局的,比如用户状态处理、加入购物车等
}(window, jQuery, Core, easyNav);

! function(e) {
    //easySlider,幻灯片逻辑,比较简单的,但做法值得借鉴
    //这可以学习一下,不知道是自己写的还是第三方库改的
    e.fn.easySlider = function(t) {}
}(jQuery);
! function(e, t, a, r) {
    //也是一大堆业务逻辑,首页用到的 Core.quickInit 方法
    t.extend(a, {
        quickInit: function() {
            //code
        },
        //code
        myInit: function() {
            //code
        },
    })
}(window, jQuery, Core);
! function(e, i, o, t) {
    //签到,领取优惠券等逻辑
    i.extend(o, {
        yigouPop: function() {
            //code
        },
        getCoupon: function(e, t) {
            //code
        },
        //code
    })
}(window, jQuery, Core);
! function(t, e, i, n) {
    e.extend(i, {
        checkIsNewer: function() {
            //code
        },
        initTemplates: function() {
            //code
        },
        //code
    })
}(window, jQuery, Core);
到这里,我们基本得到了网易考拉的js业务框架模型,就是基于jQuery进行的扩展,然后定义名为‘ Core’的基类,后面均是继承这个基类来扩展业务,其实没有想象的那样复杂,反而非常简单。

简单就是高效,这是值得学习的,做架构就要将复杂的业务简单化。

但问题是如何维护这样的代码?开发中肯定不是长成这样子的。团队配合开发,一定是分模块的,除非考拉只有一两个前端,这显然不合理嘛!如果要想得到这样的结果,并且容易维护,那么它的原来面目是怎样的呢?

我画一个乱猜js目录结构示意图(重申,是乱猜的):
&amp;lt;img src=&quot;https://pic2.zhimg.com/fcaafa4fe01f5e1bb89ba43dd5bed2c5_b.jpg&quot; data-rawwidth=&quot;1516&quot; data-rawheight=&quot;1095&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;1516&quot; data-original=&quot;https://pic2.zhimg.com/fcaafa4fe01f5e1bb89ba43dd5bed2c5_r.jpg&quot;&amp;gt;(请点击放大) (请点击放大)

在考拉的js源码里面,我们没有发现AMD或CMD的模块化身影,模块都是同步加载的,那么在开发中是如何调试呢?每一个js文件就写一个src请求吗?显然不合理。那么,他们是怎么做的呢?

现在的前端本地都应该安装了nodejs环境了的,这是前提,那么能想方法有多:

方式1:
grunt直接本地combo,在开发状态下就如图所示,直接生成一份没有压缩的代码,比如core_debug.js,这个放置在某个地方,前端开发就直接构造URL,例如
<script src="http://localhost:8080/build/core_debug.js"></script>
<script src="http://localhost:8080/build/index_debug.js"></script>
在开发状态下只需要弄一个grunt的任务监控,监控模块的变化,一旦有修改就马上生成新的,开发人员F5刷新一下页面就能看到页面了,或者开一个自动刷新页面的grunt任务。

方式2:
前端架构师写一个本地的在线combo服务器,服务器指向前端源码目录,然后让开发人员自己构造请求,然后combo服务器自动返回结果。类似这样:
<script src="http://localhost:3333/??core/baseFn.js,core/core.js,core/core-init.js"></script>
我以前写过名为‘一款淘宝风格的静态资源在线COMBO服务器’,有兴趣可以试试 Static-combo,支持css和js。

方式3:
借助后端开发语言(一般就是php了,其实nodejs也可以),建立一个具备后端语言抽象能力的前端开发调试环境,然后弄一个js初始化的函数,比如写成这样:
<%- init_js('core/baseFn.js,core/core.js,core/core-init.js') %>
输出的结果就如同上面的就可以了,这样的函数很容易实现的。其他语言版本也一样的。

方法4、5、6、7、8……方法很多了,就不赘述了。

开发好了之后,如何发版?如何解决由于内容摘要变化而引起的js引用路径变化的问题呢?这方面的原因和解析,可以参考FIS的做法,

好了,这个问题基本到这里就超出了话题的范围。再深入,估计都没人再看了。

(四)之前的一些疑问解答

1,先针对VIP业务的几个问题

问题1:当我们要在页面中使用客户端渲染页面时,而渲染的模板结构代码直接书写在页面中,JS怎样布置才合理?


很显然,由于模板是在页面中的,那么在这些代码之前最好将核心类库以及js模板引擎库放在它们的前面,就是这两段script代码。
<script src="//s2.vipstatic.com/js/public/jquery-1.10.2.js?12015091101" type="text/javascript"></script>
<script src="//s2.vipstatic.com/js/public/core3.js?12015091101" type="text/javascript"></script>
看了一下,VIP用的是CMD模块化方案来实现core3.js,这个文件内部就是很多CMD模块合并起来的底层类库,挂载在VIPSHOP这个全局对象下,有部分公共方法则挂载在jQuery下,比如前端MVC渲染引擎就是jQuery.Template,这里的核心类库的实现方法很有意思,也是一种比较独特的实现,值得学习。

不过在这里有一个做法让我百思不得其解,访问量如此之大的为啥还是采用覆盖式发布前端代码?跪求大神出来解答。静态资源覆盖式发布这个话题,在 大公司里怎样开发和部署前端代码?这里有分析解答,我就不赘述了。这里只探讨当页面中存在前端模板时,我们到底如何布置js的位置。

这里需要补充一个知识点“ HTML <script> 标签的 type 属性”,type属性并没有包含“text/html”这个类型,也就是当页面碰到这个 script标签时并不会当做js来处理,而是作为一个display=none的标签不做渲染。

但是VIP页面上存在大量这样的标签,我们其实也知道这是一个前端模板引擎,需要替换为页面结构内容才有用,但是前提是有可以替换的内容才行。为此,在VIP的首页html文档上存在大量的后端输出的json内容,存储在 VIPTE 这个对象下。当用户获得html之后,这些内容就可用作渲染的数据了。
&amp;lt;img src=&quot;https://pic1.zhimg.com/04d422e1b37c6a0406fc0fa6bfdf6dcc_b.png&quot; data-rawwidth=&quot;767&quot; data-rawheight=&quot;368&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;767&quot; data-original=&quot;https://pic1.zhimg.com/04d422e1b37c6a0406fc0fa6bfdf6dcc_r.png&quot;&amp;gt;
在这种情况下,为了更快地将 script type="text/html" 标签内的东西渲染出来,那么就必须尽早部署前端的MVC渲染引擎,也就是core3.js,但这个文件又是基于jQuery的,jQuery又必须在它之前。这就是为什么要将这两个js放在 head 内第一时间加载的原因了。

当然,将js部署在head第一时间发起下载请求,但是之前我已经说过了,如果要以极限的速度呈现首屏的内容,js放在head中并不是最佳的做法,但由于需要处理前端MVC的模板,不得已才将其放在了这里,这对于VIP的业务而言才是最合理的。

问题2:这样的模板维护起来容易吗?有没有更好的方法处理前端MVC模板?

很显然,VIP的view层模板引擎系统应该是PHP提供的语言支持,可能是由前端和后端来共同维护,那么这样将大量的前端MVC模板书写在后端的模板上,在我看来这是不太容易维护的,因为前后端都可能要对其进行修改,即便主要由前端开发人员来维护,可能也会频繁改动,如果是多人维护的就非常容易导致代码冲突。

当然,如果利用php模板包含的方式进行一定的抽象是可以将冲突的风险降低,但是还是可能被后端修改,这依然不是最完美的解决方案。而且,这种前端MVC的方式来做页面的渲染存在SEO问题,既然如此,前端MVC模板是否直接打印在html文档上,还是放在其他地方,对于搜索引擎而言都是一样的。

那么,有没有更好的方式来处理前端模板引擎呢?

我认为,可以将这样的模板直接编译成js对象,不再通过抓取DOM的方式来获取模板内容,这样做至少有以下几个好处:
  • 可以省去DOM遍历这一个耗费性能的过程,DOM操作一直以来都是耗费性能的,不是吗?
  • 从服务端模板抽离出来之后,单独由前端来维护还可以降低代码冲突,进而降低前后端协作开发的耦合度。
  • 模板编译成js之后,可以放入到CDN进行内容分发,而不再是从服务器端输出,不仅可提高用户加载的效率,还可以减小服务端输出的文档大小,降低带宽消耗。
  • 一旦这样实施了,那么jquery和core3.js就没有必要放 head 内做前置加载了,不是吗?
很显然,如果有办法实施这样的做法,那将是一举多得的,问题是如何去实现呢?

问题3:如何将前端MVC模板html编译成js对象?构建框架如何设计?

事实上,当我们需要使用前端MVC的时候,都会存在这样一个问题,我负责的项目也一样碰到了!这里涉及前端构建框架的设计了,深层次的需求其是要解决 前端MVC模板html的开发维护问题,为什么这么说?


因为开发人员不可能直接维护这样的东西:

&amp;lt;img src=&quot;https://pic1.zhimg.com/865671ae765d41aacf06cfe8771f64d4_b.png&quot; data-rawwidth=&quot;1440&quot; data-rawheight=&quot;900&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;1440&quot; data-original=&quot;https://pic1.zhimg.com/865671ae765d41aacf06cfe8771f64d4_r.png&quot;&amp;gt;我们将前端mvc模板编译成js,这只是我们想要的结果,但绝非是过程,开发人员维护的应该是容易看懂的html源文件,然后通过构建编译成上图中的js对象。 我们将前端mvc模板编译成js,这只是我们想要的结果,但绝非是过程,开发人员维护的应该是容易看懂的html源文件,然后通过构建编译成上图中的js对象。

我给出的做法是这样:
&amp;lt;img src=&quot;https://pic2.zhimg.com/0aabe4b696f41ca2c8dc9d6741d03849_b.jpg&quot; data-rawwidth=&quot;1110&quot; data-rawheight=&quot;781&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;1110&quot; data-original=&quot;https://pic2.zhimg.com/0aabe4b696f41ca2c8dc9d6741d03849_r.jpg&quot;&amp;gt;这里需要注意的是,html修改后即时编译,编译后如何快速调试,这个过程的开发体验不好,前端开发人员是不会接受这种间接的模板维护机制的。 这里需要注意的是,html修改后即时编译,编译后如何快速调试,这个过程的开发体验不好,前端开发人员是不会接受这种间接的模板维护机制的。

同理,很多带UI的js控件或插件,也可以建立类似LEES/SASS to Javasctipt的维护机制,把css自动编译成js模块,进而实现页面独立空间的js化,而开发维护的源码又是前端人员熟悉不过的html或less。

事实上,这种机制有点类似react.js的做法,只是在我建立这种机制的时候,还不晓得存在react.js这种东西。也罢,我不否认react.js的理念真的很牛逼,但它想要实现的东西咱也有自己的做法,也一样能够解决问题,不用崇洋媚外。

从这个角度看,VIP的前端业务的实现完全可以考虑迁移至React.js来实现。当然,这只是我的个人看法,瞎猜的。

2,JS的同步加载和异步加载对于TTI的影响

其实不管我们将js放在heah部分还是body结束之前,都可以使用异步加载js,这种加载方式并不占用浏览器默认发起的http请求,因此可以实现某种意义上的多个js同步下载,这看起来对于加快js脚本的下载是有好处的,但事实上是这样的吗?

当我们采用这种异步加载机制时,达到多个js同步开始下载,但由于每个js的大小是不一样的,而且网络状态也并非时时刻刻稳定的,那么浏览器并不知道同步开始下载的N个js何时才下载完成。为了解决这个问题,就需要一种机制来监控或处理js下载队列,并通过轮询的方式保障所监控的js已经下载完成,这样才能用它了处理业务。

这就是AMD或CMD这两种规范要解决的问题,它们都是从commonJS规范演变出来的针对浏览器的实现,分别对应的代表或代表类库就是我们熟悉的Require.JS和Sea.JS。

这两者的区别不是这个话题要讨论的,事实上,只有在异步模式下,两者才会有所区别,如果是combo成同步,就没有区别了。在这里我参数从两个维度来分析两者的区别,可能组合有以下6种:
&amp;lt;img src=&quot;https://pic1.zhimg.com/362ff90d882b8171c5a49635d7ef9e80_b.png&quot; data-rawwidth=&quot;511&quot; data-rawheight=&quot;263&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;511&quot; data-original=&quot;https://pic1.zhimg.com/362ff90d882b8171c5a49635d7ef9e80_r.png&quot;&amp;gt;
  • A组合:将所有js放在头部直接请求,这种是很多传统网站的做法,估计现在没有多少电商是这样做吧?当然也不是没有,海淘电商还有一家,很知名。在我看来,技术储备的不足至少是其被贝贝网超越的很重要原因之一。
  • B组合:这就是考拉网和我们的做法,也是雅虎军规提倡的做法;
  • C组合:很多刚刚接触require.js或sea.js的前端开发者常用的方式之一,我开始也这么用require.js,估计一开始这样使用异步js的人不在少数;
  • D组合:很多刚刚接触require.js或sea.js的前端开发者常用的方式之二,我曾经这么用require.js,废话同上;
  • E组合:异步加载器和核心类库放在头部,其他当前页面模块或插件异步加载,这是VIP所采用的方式;
  • F组合:异步加载器和核心类库放在尾部,其他当前页面模块或插件异步加载,这其实是贝贝网的这种做法。
在这么多种组合中,A组合可以不用拿出来谈了,已经证明最不合适的做法;B组合也谈得非常详细了,还是俩聊其他组合。E组合也有详细的分析,我不想再废话;这里主要谈C、D和F这三种组合。

其实C和D这两种形式PK,一定是D更优化,对比的情形和AB两种对比是一样的,也就是我这里只需要对比BD这两种做法即可。在这里,我只能用自己负责的项目来对比,如图:
&amp;lt;img src=&quot;https://pic1.zhimg.com/795b36081ef095f0126ff88444efce68_b.jpg&quot; data-rawwidth=&quot;749&quot; data-rawheight=&quot;216&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;749&quot; data-original=&quot;https://pic1.zhimg.com/795b36081ef095f0126ff88444efce68_r.jpg&quot;&amp;gt;在这两种模式下,同一个页面的在打开后,不做任何下滑的动作,网络请求的状态对比如下: 在这两种模式下,同一个页面的在打开后,不做任何下滑的动作,网络请求的状态对比如下:
&amp;lt;img src=&quot;https://pic2.zhimg.com/161d17abeefc7fd73eb4f5fc525ab581_b.png&quot; data-rawwidth=&quot;791&quot; data-rawheight=&quot;194&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;791&quot; data-original=&quot;https://pic2.zhimg.com/161d17abeefc7fd73eb4f5fc525ab581_r.png&quot;&amp;gt;异步模式下,要下载的资源总体积要稍微大一点点,这是由于异步请求的js没有压缩造成的,但总体区别不是很大。 异步模式下,要下载的资源总体积要稍微大一点点,这是由于异步请求的js没有压缩造成的,但总体区别不是很大。

在这种状态下,分别模拟1Mbps、2Mbps、4Mbps、30Mbps的网络请求,统计domContentLoaded时间(蓝线,Dom准备时间)和load时间(红线,window load时间),详细数据如下:
&amp;lt;img src=&quot;https://pic2.zhimg.com/6b8090e776c88ac11ab08ef668fa3521_b.png&quot; data-rawwidth=&quot;790&quot; data-rawheight=&quot;661&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;790&quot; data-original=&quot;https://pic2.zhimg.com/6b8090e776c88ac11ab08ef668fa3521_r.png&quot;&amp;gt;将平均值抽取出来,对比如下: 将平均值抽取出来,对比如下:
&amp;lt;img src=&quot;https://pic3.zhimg.com/69bca3989928ce2c3684ff8acf990202_b.png&quot; data-rawwidth=&quot;806&quot; data-rawheight=&quot;444&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;806&quot; data-original=&quot;https://pic3.zhimg.com/69bca3989928ce2c3684ff8acf990202_r.png&quot;&amp;gt; &amp;lt;img src=&quot;https://pic2.zhimg.com/cbcd517ed29721bf5844b08705a7f301_b.png&quot; data-rawwidth=&quot;818&quot; data-rawheight=&quot;476&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;818&quot; data-original=&quot;https://pic2.zhimg.com/cbcd517ed29721bf5844b08705a7f301_r.png&quot;&amp;gt;
从对比可以看出,异步请求时可以更快获取DomLoaded明显快一点,但整体的准备时间就要明显比同步状态要慢一些,而且弱网状态下,后者的差距就更加明显;
而同步模式下,虽然获得并解析Doc文档内容的时间稍微慢一点点,但是最后所有资源load完的时间则要明显快于异步的模式。

当然,两则要去除异步请求没有js没有压缩的影响,但是要要明白一点,同步模式下,我们在书写js逻辑时,是没有必要等待DomReady才去执行逻辑的,即chrome的蓝线之前也就是在即可执行js逻辑了,提供交互,在之前我有用lazyload做过演示,我们对于这个时间的控制力度是非常容易掌控的。

但是异步则不然,因为在Dom loaded蓝线之后,异步发起的请求可能还并没有完全下载完成,对于异步的那些模块如何才下载完成,我们是不知道到,但一定是在蓝线之后,而在红线之前的某个时刻,模块越多、网速越卡,这个时间就越不准确。

当然,最佳的网络状态下,Dom loaded蓝线好了之后,所有模块都已经准备好了,但是对于开发人员而言,我们掌控不了这个时间,只能交给不靠谱的网络去控制,这才是最大的问题!

也就是说,虽然同步模式下加载js并执行逻辑会阻塞页面后续发起的请求,我们却可以准确地控制js可执行的时间,也就是在蓝线前一点点,但必须保障js是放在底部的!

相反,虽然如果我们在页面中加载少量的js(最少就是加载异步js请求库,比如require.js),然后由这个库去发起请求,那么何时才能提供交互,TTI时间就不是在我们控制范围内了,并且在弱网状态下,异步对于TTI延迟的影响就越明显!

我在一开始就提出这样的概念——

web页面性能优化其精髓就是将浏览器基本无序的资源加载请求用js有序地控制起来,包括js本身。

而当我们使用异步去发起请求,虽然可以将dom loaded时间提前一点,但是却意味着我们失去了对异步模块的掌控,而将其何时加载完成交给不太靠谱的网络,这才是我不赞成异步作为性能优化的手段的根本原因。

我需要页面资源加载的控制权,失控是不能被接受的。
但是,这结论有什么用呢?
补充:什么是所指的“控制权”?

所谓的控制权,简单滴说就是要按开发者的意愿来进行一种程序开发思维。在这里,它包括js的加载时机,js文件或模块加载的先后顺序、页面逻辑执行的先后次序等多个方面的控制权。

js的加载时机和由js发起异步js的时机,这两个都是很容易控制的,但异步发起的js加载请求,每个用户的网络状况是不一样的,而且是不稳定的,模块加载的先后顺序就很难控制,甚至容易失控。

当模块加载的先后次序无法精确控制的情况下,页面逻辑执行的先后次序就很难控制,意味着我们失去了页面的控制权。
网络上有很多很多网站优化指南,其中一则就是用异步请求js来优化性能。这种优化技巧似乎和结论并不符合,尤其针对H5网站,这优化技巧反而让网站的体验更加糟糕,因为用户很多时候是处于不稳定的弱网环境下!

我们可以去看看淘宝H5页面( 手机淘宝)或百度手机( m.baidu.com/,这个需要模拟手机访问)的源代码,两个都是极端的同步js模式,都是直接将css、js打印在html页面上!这样虽然增加了文档的内容大小,文档下载的时间延迟了一点,但却将http请求数降到最低,并且将TTI提前了,页面可以以最快的速度响应用户的各种操作,这才是有价值的优化方案。

但是,请问我们如何维护这样的页面?这就是设计更深层次的前端开发模式的设计问题了,但已经超出了本文讨论的范畴,我就不想再赘述。

此外,有一种情况,就是 B组合和F组合,两种那个更优化?不打算用数据来分析了,没有这个条件,但理论上可以这么解析:

如果采用F组合,那么只要对于公共模块以及私有模块combo机制设计合理,公共模块combo成一个文件(加载1次,其他页面共用缓存),然后将首屏交互的js模块及其依赖计算出来合并成一个js模块并采用同步加载,而其他非首屏的js模块及其依赖则合并成1个文件,再用js发起异步请求,这样做应该是电商网站最优的方案。

但是,这样的对于前端架构设计的要求就非常高了,特别是模块的依赖分析,需要精通算法,对性能优化有深刻认识以及非常熟悉业务(否则怎么知道哪些是首屏交互的模块呢),这样的人才请问到哪里找啊?

基于此,这就是我最终还是采用B方案来设计我们的前端构建框架的原因,简单方便。同样地,网易考拉的前端架构师也估计是这么考虑的。

---写在最后---

在我完成这个话题之后,不过时间已经有点晚了,我还没有下班,回到家就没再关注这个话题,但总觉得还存在问题,一早就爬起来看,好在已经有人发现问题了, 很感谢 水歌同学的指正!

不管怎样,在回复这个话题的过程中,我也学到了很多东西,进一步加深了自己对于页面优化的理解 总之,感谢题主提出这样好话题,也感谢各位的关注。

(完)
-----
转载必须署名!


原文地址:https://www.zhihu.com/question/34147508


  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值