最近工作中的一个项目在运行时出现了一些性能问题。为此,我看了很多性能优化相关的内容。让我简单分享一下。前端性能优化,包括CSS/JS性能优化、网络性能优化等。《高性能网站建设指南》、《高性能网站建设进阶指南》、《高性能JavaScript》等书籍都做了很多解释,强烈推荐阅读。(有关这些书籍的列表,请参阅本文末尾)上述大部分书籍都包含以下内容,因此您可以考虑阅读这些书籍以获得完整的理解。对于本文,请不要继续阅读。如果你非要看到这里,那就说说我遇到的一些前端性能问题,说说解决方法吧。1优先优化对性能影响最大的部分当应用程序出现性能问题时,不要在代码上一往无前,先想想对性能影响最大的部分***。优先考虑那些对性能有很大影响的部分可以产生立竿见影的效果。使用ChromeDevTools,您可以快速找到导致性能不佳的最重要因素。关于ChromeDevTools的使用,强烈推荐阅读谷歌开发者的系列教程——ChromeDevTools。另外,在优化代码的时候,首先要注意那些有循环或者高频调用的地方。有时候我们可能不知道某个地方会不会被频繁执行,比如某些事件的回调。这时候可以使用console.count来统计执行次数。当这部分频繁执行的代码已经足够优化时,就要考虑是否可以减少执行次数。比如一个时间复杂度为O(n*n*n)的算法,无论怎么优化,都不如改成O(n*n)那么快。2高频触发事件的Throttle或debounce对于Scroll、Touchmove等事件,永远不要低估它们的执行频率。在处理这类事件时,可以考虑是否给它们加一个throttling或者debounce回调。throttling和debounce,可能别人不这么翻译,其实就是debounce和throttle的两个函数。Debounce和throttle是两种相似(但不完全相同)的技术,用于控制函数在特定时间段内执行的频率。您可以在下划线或lodash中找到这两个函数。2.1使用debounce对连续多次调用进行debounce,最后实际上只会调用一次。想象一下,你在电梯里,门即将关上。这时,又来了一个人,取消了关门的操作。过了一会儿,门又关上了,又来了一个人,再次取消了关门的操作。电梯会一直延迟关门的操作,直到一定时间内没有人再来。因此,debounce适合用于验证用户输入内容等场景。多个触发器只需要响应最后一个触发器。2.2Throttle使用throttle将频繁调用的函数限制在给定的调用频率。它保证无论一个函数被调用多频繁,在给定的事件中它只能被调用一次。例如,滚动时,需要检查当前滚动位置来显示或隐藏返回顶部按钮。这时候可以使用throttle来限制滚动回调函数每300ms执行一次。需要说明的是,这两个功能经常被误用,很多时候当事人并没有意识到误用。我用错了,我也看到别人用错了。这两个函数都接受一个函数作为参数,然后返回一个节流/去抖函数。下面第二种用法是正确的用法://错误的用法,每触发一次事件,就获取一个新函数$(window).on('scroll',function(){_.throttle(doSomething,300);});//正确用法,使用throttled函数作为回调$(window).on('scroll',_.throttle(doSomething,200));3JavaScript很快,DOM很慢现在JavaScript很快,真正慢的是DOM。所以避免使用一些不可读但表示可以提高写入速度。不久前,一位朋友告诉我,使用“+”号将字符串转换为数字比使用parseInt更快。我对此毫不怀疑,因为直觉上parseInt进行了函数调用,这很可能会更慢。我们在nodev6.3.0上做了一些验证,结果确实和我们预想的一样,但是差距有多大呢?在5亿次迭代时,使用+号的方法只快了2秒。尽管快了两秒,但实际的字符到数字的转换可能只会发生几次,所以这样做没有意义,只会让代码更难阅读。plus:1694.392msparseInt:3661.403ms真正慢的是DOM。DOM对外提供API,JavaScript可以调用这些API。两者由一座桥相连。每次过桥都会收取很多费用,所以要尽量减少过桥的次数。3.1DOM为什么慢说到这里,有必要简单说明一下浏览器使用HTML/CSS/JavaScript等资源呈现精彩页面的过程。浏览器收到HTML文档后,解析文档并开始构建DOM(DocumentObjectModel)树,然后发现文档中的样式表,并开始解析CSS构建CSSOM(CSSObjectModel)树,两者都完成后,开始构建渲染树。整个过程是这样的:每次修改DOM或其??样式,都要构建DOM树,重新计算CSSOM,得到新的渲染树。浏览器将使用新的呈现树来重新排列和重绘页面,以及合并图层。通常浏览器会分批回流和重绘以提高性能。但是当我们试图通过JavaScript获取某个节点的大小信息时,为了获取当前的真实信息,浏览器会立即进行一次重排。3.2避免强制同步布局JavaScript中读取的布局信息是上一帧的信息。如果在JavaScript中修改了页面的布局,比如给元素添加了class,那么就读取布局信息。这时候浏览器为了获取真实的布局信息,就需要对页面进行强行布局。因此,应该避免这种情况。3.3批量操作DOM当需要频繁操作DOM时,可以使用fastdom等工具。它的思路是将页面的读取和重写放入队列中,在页面重绘时分批执行,先读后重写。因为交错读取和重写可能会导致多个页面重排。使用fastdom可以避免这种情况的发生。虽然有fastdom之类的工具,但是有时候并不能从根本上解决问题。例如,在我最近遇到的一种情况下,与页面的简单交互(轻轻滚动页面)执行了数千个DOM操作。这时候核心问题就是减少DOM操作的次数。这时候就需要从代码层面去考虑,看是否有不必要的读取。其他高效操作DOM的方法可以参考《高性能JavaScript》的相关章节,也可以参考我的读书笔记《高性能JavaScript》(https://github.com/wy-ei/notebook/issues/34)4优化渲染性能浏览器通常每秒更新页面60次,每帧耗时16.6ms。为了让浏览器保持在60帧的帧率,让动画看起来流畅,就需要??保证帧率达到60fps,所以每一帧的逻辑都需要在16.6ms内完成。每一帧实际上由以下步骤组成:因此??,通常JavaScript执行时间不能超过10ms。JavaScript:改变元素样式,向DOM中添加元素等。样式:元素的类或样式发生了变化。这时候就需要重新计算元素的样式。Layout:元素的具体大小需要重新计算。Paint:Composite元素的绘制层:Mergemultiplelayers当然,并不是说这些操作每一帧都会进行。当你的JavaScript改变了某个布局属性,比如元素的宽高或者top等,浏览器会重新计算布局,重新排列整个页面。如果修改背景、颜色等只会导致页面重绘的属性,这不会影响页面的布局,浏览器会跳过计算布局(layout)的过程,只重绘(paint)。如果修改不需要计算布局或重绘的属性,只会合并图层,这是开销最小的修改。从https://csstriggers.com/可以知道,修改那些样式属性会触发(Layout、Paint、Composite)中的那些操作。4.1将渐变或动画元素放入单独的绘图层绘图不是在单个画布上完成的,而是在多个图层上完成的。所以,将那些会变化的元素提升到单独的一层,可以让他的变化影响到更少的元素。你可以使用will-change:transform;或转换:translateZ(0);在CSS中将元素提升到单独的层中。调试时,您可以在ChromeDevTools的时间轴窗格中查看绘图层。当然,并不是说层数越多越好,因为增加一个新的层可能会消耗额外的内存。而添加新层的目的是为了防止一个元素的变化影响到其他元素。4.2降低绘制复杂度某些属性的重绘相对复杂一些,比如filter、box-shadow等滤镜或者渐变效果。所以不要滥用这种效果。5优化JavaScript的执行下面说到的JavaScript优化并不是说如何让JavaScript执行得更快,而是如何让JavaScript更高效地与DOM协同工作。5.1使用requestAnimationFrame更新页面我们希望在每一帧开始时改变页面,目前只有使用requestAnimationFrame可以保证这一点。使用setTimeout或setInterval触发更新页面的功能。这个函数可能会在一帧的中间或者末尾调用,会导致该帧之后需要做的事情没有完成,造成丢帧。requestAnimationFrame会在页面重绘之前调度任务,保证动画有足够的时间执行JavaScript。5.2使用WebWorker处理复杂计算JavaScript是单线程的,并且可能会保持单线程,因此JavaScript在执行复杂计算时很可能会阻塞线程,导致页面卡顿。但是WebWorker的出现让我们以另一种方式获得了多线程的能力,复杂的计算可以在worker中进行。当计算完成后,将结果以postMessage的形式发回。对于单个函数,由于WebWorker接受脚本的url作为参数,使用URL.createObjectURL方法,我们可以将函数的内容转换为url并使用它来创建worker。varworkerContent=`self.onmessage=function(evt){//...//这里计算复杂varresult=complexFunc();//返回结果给self.postMessage(result);};`//获取urlvarblob=newBlob([workerContent]);varurl=window.URL.createObjectURL(blob);//创建workervarworker=newWorker(url);5.3使用transform和opacity完成动画现在只修改这两个属性,不需要经过layout和paint过程。6优化CSSCSS选择器是从右到左匹配的,所以最后一个选择器常被称为key选择器,因为最后一个选择越特殊,需要匹配的次数就越少。请避免使用*(通用选择器)作为键选择器。因为可以匹配所有元素,所以倒数第二个选择器也会匹配所有元素一次。这导致效率非常低。/*Don'tdothis*/divp*{}另外first-child这样的伪类选择器不够特殊,作为key选择器也应该避免。键选择器越特殊,浏览器可以使用更少的匹配来找到要匹配的元素,选择器的性能就越好。再说一句陈词滥调,不要使用太多选择器。如果还有同学不幸想要兼容低版本的IE,应该避免使用CSS表达式。它的性能很差。具体可以参考我之前记录的一篇笔记《高性能网站建设指南》note(https://github.com/wy-ei/notebook/issues/15)7合理处理脚本和样式表现在有了requirejs等工具而webpack,可能很少会在页面中加载大量的JavaScript/CSS代码。尽管如此,还是值得讨论一下如何正确处理脚本和样式表。大多数人已经知道,JavaScript通常放在文档的底部,而CSS放在文档的顶部。为什么?因为JavaScript会阻塞页面的解析,而外部样式表会阻塞页面的渲染和JavaScript的执行。7.1CSS阻塞渲染CSS通常被认为是一种渲染阻塞资源。在CSSOM构建之前,页面不会被渲染。将它放在顶部可以让样式表尽快开始加载。但是如果把介绍样式表的链接放在文档的底部,虽然可以立即显示页面,但是当页面加载时,就会没有样式,容易混淆。稍后加载样式表时,页面会立即重新绘制,这就是通常所说的闪烁。7.2JavaScript阻塞文档解析当在HTML文档中遇到script标签时,控制权将交给JavaScript,直到JavaScript被下载并执行后,HTML才会被解析。因此,如果将JavaScript放在文档的最前面,恰好此时JavaScript脚本加载很慢,用户会等待很长时间。这段时间HTML文档还没有解析到body部分,页面会是一片空白。另一个经常被忽视的事实是,JavaScript直到浏览器下载并解析使用链接引入的CSS文件后才会执行,因为JavaScript可能需要读取样式,而样式表尚未加载。所以浏览器不会执行JavaScript。JavaScript可以标记为async,意思是JavaScript的执行不会读取DOM,JavaScript不会被CSS阻塞,可以在空闲时间立即执行。综上所述,您必须确保CSS文件加载速度足够快。关于这部分内容,《高性能网站建设指南》有非常精彩的讲解,墙裂推荐。《高性能网站建设指南》我边看边做笔记,可以看这里。***强烈推荐阅读有关性能优化的GoogleDevelopers系列(https://developers.google.com/web/fundamentals/performance)。8篇参考文献《高性能网站建设指南》(豆瓣8.7分,https://book.douban.com/subject/3132277/)《高性能网站建设进阶指南》(豆瓣8.9分,https://book.douban.com/subject/4719162/)《高性能JavaScript》(豆瓣8.9分,https://book.douban.com/subject/5362856/)GoogleDevelopers(https://developers.google.com/web/)EfficientJavaScript(https://dev.opera.com/articles/efficient-javascript/?page=3#reflow)加速网站的最佳实践(https://developer.yahoo.com/performance/rules.html)
