摘要】本文为 Google Chrome 团队的开发项目工程师 Addy Osmani 在PerfMatters 2019 网页性能大会发表的“JavaScript性能优化”(https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4)的演讲,其分享了处理 JavaScript 的脚本优化建议,大幅地减少了下载时间和执行时间。
视频地址:https://youtu.be/X9eRLElSW1c(需科学上网)
作者 | Addy Osmani
译者 | 苏本如 责编 | 屠敏
出品 | CSDN(ID:CSDNnews)
以下为译文:
在过去的几年中,由于浏览器的脚本解析和编译速度的提高,Javascript成本构成发生了巨大的变化。到了2019年,处理Javascript的开销主要体现在脚本下载时间和CPU执行时间上。
如果浏览器的主线程忙于执行Javascript脚本,则用户交互体验可能会受影响,因此,优化脚本执行时间并消除网络瓶颈,会对用户体验产生积极的作用。
高层级的实用指南
这对Web开发人员来说意味着什么?意味着解析(Parse)和编译(Compile)不再像我们曾经想象的那么慢了。所以开发人员在优化Javascript包时,要重点关注以下三大方面:
减少下载时间
确保Javascript包尽可能地小,特别是对于移动设备。较小的包可以提升下载速度、降低内存使用量,并减少CPU开销。
避免只有一个大的Javascript包;如果包大小超过50–100 KB,就将其拆分为几个小包。(借助HTTP/2协议的多路复用机制,多个请求和响应消息可以同时传输,从而减少额外请求的开销。)
对于移动设备上使用的Javascript包更要尽可能地小,一方面因为网络带宽的制约,另一方面需要要尽量减少内存的使用。
缩短执行时间
避免持续占用主线程并影响页面响应时间的长时任务,现在脚本下载后的执行时间成为主要的成本开销。
避免使用大型内联脚本(因为它们仍然需要在主线程上进行解析和编译)。
建议参考一条经验法则:如果一个脚本超过1KB,就不要将其内联(因为当外部脚本大小超过1KB时,就会触发代码缓存)。
为什么下载和执行时间很重要?
为什么优化下载和执行时间对我们很重要?因为对于低端网络而言,下载时间的影响非常之大。尽管4G(甚至5G)在全球范围内增长迅速,但大多数人的有效连接速度仍然远远低于网络的标称速度。有时当我们外出时,会感觉到网速下降到只有3G的速度(甚至更糟)。
JavaScript的执行时间对于CPU较慢的低端手机也非常重要。由于CPU、GPU,和散热限制的不同,高端和低端手机的性能差距巨大。这对JavaScript的性能影响明显,因为它的执行受到CPU性能的制约。
事实上,在Chrome之类的浏览器上,JavaScript的执行时间可以达到页面加载总耗时的30%。下图是一个具有典型工作负载的网站(Reddit.com)在一台高端桌面PC上的页面加载情况分析:
V8引擎下的Javascript处理时间占整个页面加载时间的10-30%
对于移动设备,与高端手机(如Pixel 3)相比,在中端手机(如Moto G4)上执行Reddit的Javascript脚本需要3-4倍的耗时,而在低端手机(价格低于100美元的Alcatel 1X)上执行Reddit的Javascript脚本更是需要6倍以上的耗时:
Reddit的Javascript脚本在几种不同设备(低端、中端和高端)上的执行时间。
注意:Reddit对于桌面和移动网络有不同的体验,因此MacBook Pro的执行结果无法与其他结果进行比较。
当你着手优化JavaScript的执行时间时,你需要留意可能长时间独占界面线程(UI Thread)的长时任务。即使页面看起来已经加载完成,这些长时任务也会拖累关键任务的执行。把长时任务分解成较小的任务。通过拆分代码并确定加载顺序,你可以更快地实现页面交互,并有望降低输入延迟。
独占主线程的长时任务应该拆分。
V8引擎如何提高Javascript解析/编译速度?
自Chrome 版本60以来,V8引擎的原始JS的解析速度增加了2倍。与此同时,Chrome还做了其他工作一些工作使得解析和编译工作并行化,这使得这部分的成本开销对用户体验的影响变得不是那么显著和关键了。
V8引擎通过将解析和编译工作转到worker线程上,使得主线程上的解析和编译工作量平均减少了40%。例如,Facebook降低了46%,Pinterest降低62%,而最大的改进是是YouTube ,降低了81%。这是在现有的非主线程流解析/编译性能改进基础上的进一步提升。
不同版本的V8引擎的解析时间对比
我们还可以图示对比不同Chrome版本的不同V8引擎对CPU处理时间的影响。可以看出,Chrome 61解析Facebook的JS脚本所花费的时间,可以供Chrome 75解析同样的Facebook的JS脚本,和6个Twitter的JS脚本了。
Chrome 61解析Facebook的JS脚本所花费的时间,可以供Chrome 75解析完成同样的Facebook的JS脚本,和6个Twitter的JS脚本了。
让我们深入研究一下这些改进是如何实现的。总的来说,脚本资源可以在worker线程上进行流式解析和编译,这意味着:
V8引擎可以在不阻塞主线程的情况下解析和编译JavaScript。
当整个HTML解析器遇到<script>标记时,就开始流式处理。遇到阻塞解析器(parse-blocking)的脚本时,HTML解析器就放弃,而对于异步脚本则继续处理。
在大多数网络连接速度下,V8引擎的解析速度都比下载速度快,因此在最后一个脚本字节被下载后几毫秒的时间内,V8引擎就能完成解析+编译工作。
具体来说,很多老版本的Chrome在开始脚本解析之前,需要将脚本下载完成,这是一种简单的方法,但它没有充分利用CPU的能力。而从版本41到68,Chrome在下载一开始时就立即在单独的线程上解析异步和延迟脚本。
JS脚本以多个块下载。V8引擎看到大于30KB的脚本被下载后就会启动脚本流解析工作。
Chrome 71采用了基于任务(task-based)的设置方案。调度器可以一次解析多个异步/延迟脚本,这一改进使得主线程解析时间缩短了约20%,真实网站上的TTI/FID整体提高了大约2%。
Chrome 71采用了基于任务(task-based)的设置,调度器可以一次解析多个异步/延迟脚本
Chrome 72开始采用流式处理作为主要的解析方式,现在常规的同步脚本(内联脚本除外)也可以采用这种解析方式。如果主线程需要,我们也可以继续采用基于任务的解析,从而减少不必要地重复工作。
旧版的Chrome支持流式解析和编译,其中来自网络的脚本源数据必须先到达Chrome主线程后,再转发给流解析器解析。
这通常会导致这样的情况:脚本数据已经从网络上下载完成,但由于主线程上的其他任务(如HTML解析、排版或者JavaScript执行),阻塞了脚本数据的转发,因此流解析器(streaming parser)不得不空等。
现在我们正尝试在预加载时开始解析,以前主线程反弹会阻碍这种操作。
Leszek Swirski 在 BlinkOn 10 上的演讲介绍了相关细节:https://youtu.be/D1UJgiG4_NI(需科学上网)
这些改变如何反映到DevTools中?
除上述之外,DevTools中还存在一个问题,它以表明它会独占 CPU(完全阻塞)的方式渲染整个解析器任务。但是,不管解析器是否需要数据(数据需要通过主线程)都会阻塞。当我我们从单个流线程转向多个流传输任务时,这个问题变得非常明显。下面是你在Chrome 69中看到的情况:
DevTools以表明它会独占CPU(完全阻塞)的方式渲染整个解析器任务
如上图示,“解析脚本”任务需要1.08秒。但是解析JavaScript其实并没有那么慢!大部分时间除了等待数据通过主线程之外什么都做不了。
而在Chrome 76中显示的内容就不一样了:
在Chrome 76中,解析工作被分解为多个较小的流任务。
一般来说,DevTools性能窗格非常适合从宏观层面分析你的页面。对于更具体的V8度量指标,如Javascript解析和编译时间,我们建议使用带有运行时调用统计(RCS)的Chrome跟踪工具。在RCS结果中,Parse-Background和Compile-Background会告诉你在主线程外解析和编译Javascript花费了多少时间,而Parse和Compile是针对主线程的度量指标。
这些改变对现实应用的影响是什么?
让我们来看一些真实网站的示例,来了解脚本流(script streaming)是如何工作的。
主线程和worker线程在MacBook Pro上解析和编译Reddit网站的JS所花费的时间对比
Reddit.com网站有几个超过100KB的JS包,它们包装在外部函数中,导致在主线程上需要进行大量的延迟编译(lazy compilation)。如上图所示,主线程耗时才是真正关键的,因为主线程持续繁忙会严重影响交互体验。Reddit的大部分时间花在了主线程上,而worker线程或后台线程的使用率很低。
可以将一些较大的JS包拆分为几个不需要包装的小包(例如每个包50 KB),以最大限度地实现并行化,这样每个包都可以单独进行流解析和编译,并在载入期间减少主线程的解析/编译时间。
主线程和worker线程在MacBook Pro上解析和编译Facebook网站的JS所花费的时间对比
我们再看看像facebook.com这样的网站的情况。Facebook使用了大约292个请求,加载了大约6MB的压缩JS脚本,其中一些是异步的,一些是预加载的,还有一些是低优先级的。它们的许多脚本都非常小,粒度也不大,这有助于后台/workers线程上的整体并行化,因为这些较小的脚本可以同时进行流解析/编译。
值得注意地是,像Facebook或Gmail这样老牌的应用程序的桌面版本上有这么多的脚本可能是合理的。但是你的网站可能和Facebook不一样。不管怎样,尽可能地简化你的JS包,不必要的就不要装载了。
尽管大多数JavaScript解析和编译工作都可以在后台线程上以流式方式进行,但仍有一些工作必须在主线程上进行。而当主线程繁忙时,页面就无法响应用户输入了。所以要密切关注下载和执行代码对用户体验的影响。
注意:目前并不是所有的Javascript引擎和浏览器都实现了脚本流(script streaming)式加载优化。但是我们仍然相信,本文的整体指导会帮助大家全面地提升用户体验。
解析JSON的开销
JSON语法比JavaScript语法简单很多,所以JSON的解析效率要比Javascript高得多。基于这一点,Web应用程序可以提供类似于JSON的大型配置对象文本,而不是将数据作为Javascript对象文本进行内联,这样可以大大提高Web应用程序的加载性能。如下所示:
const data={ foo: 42, bar: 1337 }; //
……它可以用 JSON 字符串形式表示,然后在运行时进行 JSON 解析。如下所示:
const data=JSON.parse('{"foo":42,"bar":1337}'); //
只要JSON字符串只计算一次,那么相比Javascript对象文本, JSON.parse方法就要快得多,冷加载时尤其明显。
在为大量数据使用普通对象文本时还有一个额外的风险:它们可能会被解析两次!
第一次是文本预解析时。
第二次是文本延迟解析时。
第一次解析是必须的,可以将对象文本放在顶层或PIFE中来避免第二次解析。
重复访问时的解析/编译情况如何?
V8引擎的(字节)代码缓存优化可以帮助改善重复访问时的体验。当第一次请求脚本时,Chrome会下载脚本并将其交给V8引擎进行编译。同时将文件存储在浏览器的磁盘缓存中。当第二次请求JS文件时,Chrome会从浏览器缓存中获取该文件,并再次将其交给V8引擎进行编译。然而,这次编译的代码会被序列化,并作为元数据附加到缓存的脚本文件中。
V8引擎的代码缓存示意图
第三次请求脚本时,Chrome从缓存中获取脚本文件和文件的元数据,并将两者都交给V8引擎。V8引擎会反序列化元数据来跳过编译步骤。如果前两次访问间隔小于72小时内,代码缓存就会启动。如果采用service worker来缓存脚本,那么chrome也会主动启动代码缓存。详细信息可以参阅 web 开发者的代码缓存指南。
总结
到了2019年。脚本下载和执行的时间开销已经变成加载脚本的主要瓶颈。所以你应该为你的首屏内容准备一个较小的同步(内联)脚本包,其余部分则使用一个或多个延迟脚本,并且把较大的包拆分成许多小包来按需加载。这样一来就能充分利用 V8 引擎的并行化能力。
在移动设备上,由于网络、内存消耗和CPU执行时间的制约,你需要尽可能地减少脚本的数量,平衡延迟和缓存设置,尽可能地让解析和编译工作在主线程外执行。
原文:https://v8.dev/blog/cost-of-javascript-2019
本文为 CSDN 翻译,转载请注明来源出处。
【End】
白何谓Margin Collapse
不同于其他很多属性,盒模型中垂直方向上的Margin会在相遇时发生崩塌,也就是说当某个元素的底部Margin与另一个元素的顶部Margin相邻时,只有二者中的较大值会被保留下来,可以从下面这个简单的例子来学习:
.square { width: 80px; height: 80px; }.red { background-color: #F44336; margin-bottom: 40px; }.blue { background-color: #2196F3; margin-top: 30px; }
在上述例子中我们会发现,红色和蓝色方块的外边距并没有相加得到70px,而是只有红色的下外边距保留了下来。我们可以使用一些方法来避免这种行为,不过建议来说还是尽量统一使用margin-bottom
属性,这样就显得和谐多了。
使用Flexbox进行布局
在传统的布局中我们习惯使用Floats或者inline-blocks,不过它们更适合于格式化文档,而不是整个网站。而Flexbox则是专门的用于进行布局的工具。Flexbox模型允许开发者使用很多便捷可扩展的属性来进行布局,估计你一旦用上就舍不得了:
.container { display: flex; /* Don't forget to add prefixes for Safari */display: -webkit-flex; }
我们已经在Tutorialzine上提供了很多的关于Flexbox的介绍与小技巧,譬如5 Flexbox Techniques You Need to Know About。
使用CSS Reset
虽然这些年来随着浏览器的迅速发展与规范的统一,浏览器特性碎片化的情况有所改善,但是在不同的浏览器之间仍然存在着很多的行为差异。而解决这种问题的最好的办法就是使用某个CSS Reset来为所有的元素设置统一的样式,保证你能在相对统一干净的样式表的基础上开始工作。目前流行的Reset库有 normalize.css, minireset以及 ress ,它们都可以修正很多已知的浏览器之间的差异性。而如果你不打算用某个外在的库,那么建议可以使用如下的基本规则:
* { margin: 0; padding: 0; box-sizing: border-box; }
上面的规则看起来没啥用,不过如果不同的浏览器在默认情况下为你设置了不同的外边距/内边距的默认值,还是会挺麻烦的。
一切应为Border-box
虽然很多初学者并不了解box-sizing
这个属性,但是它确实相当的重要。而最好的理解它的方式就是看看它的两种取值:
默认值为content-box,即当我们设置某个元素的heght/width属性时,仅仅会作用于其内容尺寸。而所有的内边距与边都是在其之上的累加,譬如某个<div>
标签设置为宽100,内边距为10,那么最终元素会占用120(100 + 2*10)的像素。
border-box:内边距与边是包含在了width/height之内,譬如设置了width:100px
的<div>
无论其内边距或者边长设置为多少,其占有的大小都是100px。
将元素设置为border-box会很方便你进行样式布局,这样的话你就可以在父元素设置高宽限制而不担心子元素的内边距或者边打破了这种限制。
以背景图方式使用Images
如果需要在响应式的环境下展示图片,有个简单的小技巧就是使用该图片作为某个<div>
的背景图而不是直接使用img标签。基于这种方式配合上background-size
与background-position
这两个属性,可以很方便地按比例缩放:
img { width: 300px; height: 200px; }div { width: 300px; height: 200px; background: url('http://cdn.tutorialzine.com/wp-content/uploads/2016/08/bicycle.jpg'); background-position: center center; background-size: cover; }section{ float: left; margin: 15px; }
不过这种方式也是存在缺陷的,譬如你无法设置图片的懒加载、图片无法被搜索引擎或者其他类似的工具抓取到,有个不错的属性叫object-fit可以解决这个问题,不过该属性目前的浏览器支持并不是很完善。
Better Table Borders
HTML中使用Tables进行布局一直是个很头疼的问题,它们使用起来很简单,但是无法进行响应式操作,并且也不方便进行全局样式设置。譬如,如果你打算为Table的边与单元的边添加样式,可能得到的结果如下:
table { width: 600px; border: 1px solid #505050; margin-bottom: 15px; color:#505050; }td{ border: 1px solid #505050; padding: 10px; }
这里存在的问题是出现了很多的重复的边,会导致视觉上不协调的情况,那么我们可以通过设置border-collapse:collapse
来进行处理:
注释格式优化
CSS虽然谈不上一门编程语言但是其仍然需要添加注释以保障整体代码的可读性,只要添加些简单的注释不仅可以方便你更好地组织整个样式表还能够让你的同事或者未来的自己更好地理解。对于CSS中整块的注释或者使用在Media-Query中的注释,建议是使用如下形式:
/*--------------- #Header ---------------*/header { }header nav { }/*--------------- #Slideshow ---------------*/.slideshow { }
而设计的细节说明或者一些不重要的组件可以用如下单行注释的方式:
/* Footer Buttons */.footer button { }.footer button:hover { }
同时,不要忘了CSS中是没有//
这种注释方式的:
/* Do */p { padding: 15px; /*border: 1px solid #222;*/}/* Don't */p { padding: 15px; // border: 1px solid #222; }
使用Kebab-case命名变量
对于样式类名或者ID名的命名都需要在多个单词之间添加-
符号,CSS本身是大小写不敏感的因此你是用不了camelCase的,另一方面,很久之前也不支持下划线,所以现在的默认的命名方式就是使用-
:
/* Do */.footer-column-left { }/* Don't */.footerColumnLeft { }.footer_column_left { }
而涉及到具体的变量命名规范时,建议是使用BEM规范,只要遵循一些简单的原则即可以保证基于组件风格的命名一致性。你也可以参考CSS Tricks来获得更多的细节描述。
避免重复代码
大部分元素的CSS属性都是从DOM树根部继承而来,这也是其命名为级联样式表的由来。我们以font
属性为例,该属性往往是继承自父属性,因此我们并不需要再单独地为元素设置该属性。我们只需要在html
或者body
中添加该属性然后使其层次传递下去即可:
html { font: normal 16px/1.4 sans-serif; }
使用transform添加CSS Animations
不建议直接改变元素的width
与height
属性或者left/top/bottom/right
这些属性来达到动画效果,而应该优先使用transform()
属性来提供更平滑的变换效果,并且能使得代码的可读性会更好:
.ball { left: 50px; transition: 0.4s ease-out; }/* Not Cool*/.ball.slide-out { left: 500px; }/* Cool*/.ball.slide-out { transform: translateX(450px); }
Transform的几个属性translate
、rotate
、scale
都具有比较好的浏览器兼容性可以放心使用。
不要重复造轮子
现在CSS社区已经非常庞大,并且不断地有新的各式各样的库开源出来。这些库可以帮助我们解决从小的代码片到用于构建完整的响应式应用的全框架。所以如果下次你再碰到什么CSS问题的时候,在打算撸起袖子自己上之前可以尝试在GitHUB或者CodePen上搜索可行方案。
尽可能使用低优先级的选择器
并不是所有的CSS选择器的优先级都一样,很多初学者在使用CSS选择器的时候都是考虑以新的特性去复写全部的继承特性,不过这一点在某个元素多状态时就麻烦了,譬如下面这个例子:
a{ color: #fff; padding: 15px; }a#blue-btn { background-color: blue; }a.active { background-color: red; }
我们本来希望将.active
类添加到按钮上然后使其显示为红色,不过在上面这个例子中很明显起不了作用,因为button
已经以ID选择器设置过了背景色,也就是所谓的Higher Selector Specificity。一般来说,选择器的优先级顺序为:ID(#id) > Class(.class) > Type(header)
避免使用!important
认真的说,千万要避免使用!important,这可能会导致你在未来的开发中无尽的属性重写,你应该选择更合适的CSS选择器。而唯一的可以使用!important
属性的场景就是当你想去复写某些行内样式的时候,不过行内样式本身也是需要避免的。
使用text-transform属性设置文本大写
<div class="movie-poster">Star Wars: The Force Awakens</div>.movie-poster { text-transform: uppercase; }
Em, Rem, 以及 Pixel
已经有很多关于人们应该如何使用em,rem,以及px作为元素尺寸与文本尺寸的讨论,而笔者认为,这三个尺寸单位都有其适用与不适用的地方。不同的开发与项目都有其特定的设置,因此并没有通用的规则来决定应该使用哪个单位,这里是我总结的几个考虑:
em – 其基本单位即为当前元素的font-size
值,经常适用于media-queries中,em是特别适用于响应式开发中。
rem – 其是相对于html
属性的单位,可以保证文本段落真正的响应式尺寸特性。
px – Pixels 并没有任何的动态扩展性,它们往往用于描述绝对单位,并且可以在设置值与最终的显示效果之间保留一定的一致性。
在大型项目中使用预处理器
估计你肯定听说过 Sass, Less, PostCSS, Stylus这些预处理器与对应的语法。Preprocessors可以允许我们将未来的CSS特性应用在当前的代码开发中,譬如变量支持、函数、嵌套式的选择器以及很多其他的特性,这里我们以Sass为例:
$accent-color: #2196F3;a { padding: 10px 15px; background-color: $accent-color; }a:hover { background-color: darken($accent-color,10%); }
使用Autoprefixers来提升浏览器兼容性
使用特定的浏览器前缀是CSS开发中常见的工作之一,不同的浏览器、不同的属性对于前缀的要求也不一样,这就使得我们无法在编码过程中记住所有的前缀规则。并且在写样式代码的时候还需要加上特定的浏览器前缀支持也是个麻烦活,幸亏现在也是有很多工具可以辅助我们进行这样的开发:
Online tools: Autoprefixer
Text editor plugins: Sublime Text, Atom
Libraries: Autoprefixer (PostCSS)
在生产环境下使用Minified代码
为了提升页面的加载速度,在生产环境下我们应该默认使用压缩之后的资源代码。在压缩的过程中,会将所有的空白与重复剔除掉从而减少整个文件的体积大小。当然,经过压缩之后的代码毫无可读性,因此在开发阶段我们还是应该使用普通的版本。对于CSS的压缩有很多的现行工具:
Online tools – CSS Minifier (API included), CSS Compressor
Text editor plugins: Sublime Text, Atom
Libraries: Minfiy (PHP), CSSO and CSSNano (PostCSS, Grunt, Gulp)
选择哪个工具肯定是依赖于你自己的工作流啦~
多参阅Caniuse
不同的浏览器在兼容性上差异很大,因此如果我们可以针对我们所需要适配的浏览器,在caniuse上我们可以查询某个特性的浏览器版本适配性,是否需要添加特定的前缀或者在某个平台上是否存在Bug等等。不过光光使用caniuse肯定是不够的,我们还需要使用些额外的服务来进行检测。
Validate:校验
对于CSS的校验可能不如HTML校验或者JavaScript校验那么重要,不过在正式发布之前用Lint工具校验一波你的CSS代码还是很有意义的。它会告诉你代码中潜在的错误,提示你一些不符合最佳实践的代码以及给你一些提升代码性能的建议。就像Minifers与Autoprefixers,也有很多可用的工具:
Online tools: W3 Validator, CSS Lint
Text editor plugins: Sublime Text, Atom
Libraries: lint (Node.js, PostCSS), css-validator (Node.js)
(作者:Danny Markov,翻译:王下邀月熊_Chevalier)
英语原文:20 Protips For Writing Modern CSS
迎大家关注我,我会不定期分享一些自己觉得比较好的文章给大家。
【摘要】本文为 Google Chrome 团队的开发项目工程师 Addy Osmani 在PerfMatters 2019 网页性能大会发表的“JavaScript性能优化”(https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4)的演讲,其分享了处理 JavaScript 的脚本优化建议,大幅地减少了下载时间和执行时间。
以下为译文:
在过去的几年中,由于浏览器的脚本解析和编译速度的提高,Javascript成本构成发生了巨大的变化。到了2019年,处理Javascript的开销主要体现在脚本下载时间和CPU执行时间上。
如果浏览器的主线程忙于执行Javascript脚本,则用户交互体验可能会受影响,因此,优化脚本执行时间并消除网络瓶颈,会对用户体验产生积极的作用。
高层级的实用指南
这对Web开发人员来说意味着什么?意味着解析(Parse)和编译(Compile)不再像我们曾经想象的那么慢了。所以开发人员在优化Javascript包时,要重点关注以下三大方面:
减少下载时间
缩短执行时间
避免使用大型内联脚本(因为它们仍然需要在主线程上进行解析和编译)。
为什么下载和执行时间很重要?
为什么优化下载和执行时间对我们很重要?因为对于低端网络而言,下载时间的影响非常之大。尽管4G(甚至5G)在全球范围内增长迅速,但大多数人的有效连接速度仍然远远低于网络的标称速度。有时当我们外出时,会感觉到网速下降到只有3G的速度(甚至更糟)。
JavaScript的执行时间对于CPU较慢的低端手机也非常重要。由于CPU、GPU,和散热限制的不同,高端和低端手机的性能差距巨大。这对JavaScript的性能影响明显,因为它的执行受到CPU性能的制约。
事实上,在Chrome之类的浏览器上,JavaScript的执行时间可以达到页面加载总耗时的30%。下图是一个具有典型工作负载的网站(Reddit.com)在一台高端桌面PC上的页面加载情况分析:
V8引擎下的Javascript处理时间占整个页面加载时间的10-30%
对于移动设备,与高端手机(如Pixel 3)相比,在中端手机(如Moto G4)上执行Reddit的Javascript脚本需要3-4倍的耗时,而在低端手机(价格低于100美元的Alcatel 1X)上执行Reddit的Javascript脚本更是需要6倍以上的耗时:
Reddit的Javascript脚本在几种不同设备(低端、中端和高端)上的执行时间。
注意:Reddit对于桌面和移动网络有不同的体验,因此MacBook Pro的执行结果无法与其他结果进行比较。
当你着手优化JavaScript的执行时间时,你需要留意可能长时间独占界面线程(UI Thread)的长时任务。即使页面看起来已经加载完成,这些长时任务也会拖累关键任务的执行。把长时任务分解成较小的任务。通过拆分代码并确定加载顺序,你可以更快地实现页面交互,并有望降低输入延迟。
独占主线程的长时任务应该拆分。
V8引擎如何提高Javascript解析/编译速度?
自Chrome 版本60以来,V8引擎的原始JS的解析速度增加了2倍。与此同时,Chrome还做了其他工作一些工作使得解析和编译工作并行化,这使得这部分的成本开销对用户体验的影响变得不是那么显著和关键了。
V8引擎通过将解析和编译工作转到worker线程上,使得主线程上的解析和编译工作量平均减少了40%。例如,Facebook降低了46%,Pinterest降低62%,而最大的改进是是YouTube ,降低了81%。这是在现有的非主线程流解析/编译性能改进基础上的进一步提升。
不同版本的V8引擎的解析时间对比
我们还可以图示对比不同Chrome版本的不同V8引擎对CPU处理时间的影响。可以看出,Chrome 61解析Facebook的JS脚本所花费的时间,可以供Chrome 75解析同样的Facebook的JS脚本,和6个Twitter的JS脚本了。
Chrome 61解析Facebook的JS脚本所花费的时间,可以供Chrome 75解析完成同样的Facebook的JS脚本,和6个Twitter的JS脚本了。
让我们深入研究一下这些改进是如何实现的。总的来说,脚本资源可以在worker线程上进行流式解析和编译,这意味着:
具体来说,很多老版本的Chrome在开始脚本解析之前,需要将脚本下载完成,这是一种简单的方法,但它没有充分利用CPU的能力。而从版本41到68,Chrome在下载一开始时就立即在单独的线程上解析异步和延迟脚本。
JS脚本以多个块下载。V8引擎看到大于30KB的脚本被下载后就会启动脚本流解析工作。
Chrome 71采用了基于任务(task-based)的设置方案。调度器可以一次解析多个异步/延迟脚本,这一改进使得主线程解析时间缩短了约20%,真实网站上的TTI/FID整体提高了大约2%。
Chrome 71采用了基于任务(task-based)的设置,调度器可以一次解析多个异步/延迟脚本
Chrome 72开始采用流式处理作为主要的解析方式,现在常规的同步脚本(内联脚本除外)也可以采用这种解析方式。如果主线程需要,我们也可以继续采用基于任务的解析,从而减少不必要地重复工作。
旧版的Chrome支持流式解析和编译,其中来自网络的脚本源数据必须先到达Chrome主线程后,再转发给流解析器解析。
这通常会导致这样的情况:脚本数据已经从网络上下载完成,但由于主线程上的其他任务(如HTML解析、排版或者JavaScript执行),阻塞了脚本数据的转发,因此流解析器(streaming parser)不得不空等。
现在我们正尝试在预加载时开始解析,以前主线程反弹会阻碍这种操作。
Leszek Swirski 在 BlinkOn 10 上的演讲介绍了相关细节:https://youtu.be/D1UJgiG4_NI(需科学上网)
这些改变如何反映到DevTools中?
除上述之外,DevTools中还存在一个问题,它以表明它会独占 CPU(完全阻塞)的方式渲染整个解析器任务。但是,不管解析器是否需要数据(数据需要通过主线程)都会阻塞。当我我们从单个流线程转向多个流传输任务时,这个问题变得非常明显。下面是你在Chrome 69中看到的情况:
DevTools以表明它会独占CPU(完全阻塞)的方式渲染整个解析器任务
如上图示,“解析脚本”任务需要1.08秒。但是解析JavaScript其实并没有那么慢!大部分时间除了等待数据通过主线程之外什么都做不了。
而在Chrome 76中显示的内容就不一样了:
在Chrome 76中,解析工作被分解为多个较小的流任务。
一般来说,DevTools性能窗格非常适合从宏观层面分析你的页面。对于更具体的V8度量指标,如Javascript解析和编译时间,我们建议使用带有运行时调用统计(RCS)的Chrome跟踪工具。在RCS结果中,Parse-Background和Compile-Background会告诉你在主线程外解析和编译Javascript花费了多少时间,而Parse和Compile是针对主线程的度量指标。
这些改变对现实应用的影响是什么?
让我们来看一些真实网站的示例,来了解脚本流(script streaming)是如何工作的。
主线程和worker线程在MacBook Pro上解析和编译Reddit网站的JS所花费的时间对比
Reddit.com网站有几个超过100KB的JS包,它们包装在外部函数中,导致在主线程上需要进行大量的延迟编译(lazy compilation)。如上图所示,主线程耗时才是真正关键的,因为主线程持续繁忙会严重影响交互体验。Reddit的大部分时间花在了主线程上,而worker线程或后台线程的使用率很低。
可以将一些较大的JS包拆分为几个不需要包装的小包(例如每个包50 KB),以最大限度地实现并行化,这样每个包都可以单独进行流解析和编译,并在载入期间减少主线程的解析/编译时间。
主线程和worker线程在MacBook Pro上解析和编译Facebook网站的JS所花费的时间对比
我们再看看像facebook.com这样的网站的情况。Facebook使用了大约292个请求,加载了大约6MB的压缩JS脚本,其中一些是异步的,一些是预加载的,还有一些是低优先级的。它们的许多脚本都非常小,粒度也不大,这有助于后台/workers线程上的整体并行化,因为这些较小的脚本可以同时进行流解析/编译。
值得注意地是,像Facebook或Gmail这样老牌的应用程序的桌面版本上有这么多的脚本可能是合理的。但是你的网站可能和Facebook不一样。不管怎样,尽可能地简化你的JS包,不必要的就不要装载了。
尽管大多数JavaScript解析和编译工作都可以在后台线程上以流式方式进行,但仍有一些工作必须在主线程上进行。而当主线程繁忙时,页面就无法响应用户输入了。所以要密切关注下载和执行代码对用户体验的影响。
注意:目前并不是所有的Javascript引擎和浏览器都实现了脚本流(script streaming)式加载优化。但是我们仍然相信,本文的整体指导会帮助大家全面地提升用户体验。
解析JSON的开销
JSON语法比JavaScript语法简单很多,所以JSON的解析效率要比Javascript高得多。基于这一点,Web应用程序可以提供类似于JSON的大型配置对象文本,而不是将数据作为Javascript对象文本进行内联,这样可以大大提高Web应用程序的加载性能。如下所示:
const data={ foo: 42, bar: 1337 }; //
……它可以用 JSON 字符串形式表示,然后在运行时进行 JSON 解析。如下所示:
const data=JSON.parse('{"foo":42,"bar":1337}'); //
只要JSON字符串只计算一次,那么相比Javascript对象文本, JSON.parse方法就要快得多,冷加载时尤其明显。
在为大量数据使用普通对象文本时还有一个额外的风险:它们可能会被解析两次!
第一次解析是必须的,可以将对象文本放在顶层或PIFE中来避免第二次解析。
重复访问时的解析/编译情况如何?
V8引擎的(字节)代码缓存优化可以帮助改善重复访问时的体验。当第一次请求脚本时,Chrome会下载脚本并将其交给V8引擎进行编译。同时将文件存储在浏览器的磁盘缓存中。当第二次请求JS文件时,Chrome会从浏览器缓存中获取该文件,并再次将其交给V8引擎进行编译。然而,这次编译的代码会被序列化,并作为元数据附加到缓存的脚本文件中。
V8引擎的代码缓存示意图
第三次请求脚本时,Chrome从缓存中获取脚本文件和文件的元数据,并将两者都交给V8引擎。V8引擎会反序列化元数据来跳过编译步骤。如果前两次访问间隔小于72小时内,代码缓存就会启动。如果采用service worker来缓存脚本,那么chrome也会主动启动代码缓存。详细信息可以参阅 web 开发者的代码缓存指南。
总结
到了2019年。脚本下载和执行的时间开销已经变成加载脚本的主要瓶颈。所以你应该为你的首屏内容准备一个较小的同步(内联)脚本包,其余部分则使用一个或多个延迟脚本,并且把较大的包拆分成许多小包来按需加载。这样一来就能充分利用 V8 引擎的并行化能力。
在移动设备上,由于网络、内存消耗和CPU执行时间的制约,你需要尽可能地减少脚本的数量,平衡延迟和缓存设置,尽可能地让解析和编译工作在主线程外执行。
原文:
https://v8.dev/blog/cost-of-javascript-2019
*请认真填写需求信息,我们会在24小时内与您取得联系。