前言
当我谈性能优化时,我谈些什么?通常前端性能可以认为是用户获取所需要页面数据或执行某个页面动作的一个实时性指标,一般以用户希望获取数据的操作到用户实际获得数据的时间间隔来衡量。例如用户希望获取数据的操作是打开某个页面,那么这个操作的前端性能就可以用该用户操作开始到屏幕展示页面内容给用户的这段时间间隔来评判。前端性能优化的点很多,记起来特别麻烦。这是因为没搞清楚性能优化的是什么东西。下面从从输入URL
到页面展示的过程说说性能优化。
一、优化原理
从输入URL
到页面展示的过程:
- 输入网址
DNS
解析,获取IP
地址- 发起
TCP连接
,三次握手
- 发送
HTTP请求
- 服务器
处理请求
并返回HTTP报文
- 浏览器解析渲染页面
- 连接结束
1.DNS
解析
- 通过正确设置HTTP 响应头来缓存JavaScript 文件,通过向文件名增加时间戳来避免缓存问题。
- 在浏览器加载网页时,对网页中的
<link>
或者<a>
的href
属性中的域名进行后台的预解析, 并且将解析结果缓存在浏览器端。 - 使用CDN(Content Delivery Network)提供JavaScript 文件;CDN 不仅可以提升性能,它也为你管理文件的压缩与缓存。
2.发起TCP
连接
3.发送HTTP
请求
-
减少请求数,可通过合并JavaScript 和CSS 文件,或使用MXHR。
-
缩短页面的加载时间,页面主要内容加载完成后,用Ajax 获取那些次要的文件。
-
确保你的代码错误不会输出给用户,并在服务端处理错误。
-
知道何时使用成熟的Ajax 类库,以及何时编写自己的底层Ajax代码。
-
使用keep-alive或者persistent来建立持久连接,降低了延时和连接建立的开销。
-
性能分析
- 使用网络分析工具找出加载脚本和页面中其他资源的瓶颈,这会帮助你决定哪些脚本需要延迟加载,或者需要进一步分析。
- 尽管传统的经验告诉我们要尽量减少HTTP 请求数,但把脚本尽可能延迟加载可以加快页面渲染速度,给用户带来更好的体验
- 使用性能分析工具找出脚本运行过程中速度慢的地方,检查每个函数所消耗的时间,以及函数被调用的次数,通过调用桟自身提供的一些线索来找出需要集中精力优化的地方。
- 尽管耗费的时间和调用次数通常是数据中最有价值的部分,但仔细观察函数的调用过程,你也许会发现其他优化目标。
4.服务器处理请求并返回
- 合并脚本。页面中的
<script>
标签越少,加载也就越快,响应也更迅速。无论外链文件还是内嵌脚本都是如此。 - 合并JavaScript 文件以减少HTTP 请求数。
- 压缩JavaScript文件。
- 在服务器端压缩JavaScript 文件(Gzip编码)。(后端)
5.浏览器解析渲染页面
-
</body>
闭合标签之前,将所有的<script>
标签放到页面底部。这能确保在脚本执行前页面已经完成了渲染。 -
无阻塞下载
JavaScript
的方法
- 使用
<script>
标签的defer
属性 - 使用动态创建的
<script>
元素来下载并执行代码 - 使用
XHR
对象下载JavaScript
代码并注入页面中
- 执行JavaScript代码
-
数据存储:
- 访问直接量和局部变量的速度最快,相反,访问数组元素和对象成员相对较慢。
- 由于局部变量存在于作用域链的起始位置,因此访问局部变量比访问跨作用域变量更快。变量在作用域链中的位置越深,访问所需时间就越长。由于全局变量总处在作用域链的最末端,因此访问速度也是最慢的。
-
代码的写法和算法:
- 避免使用with语句,因为它会改变运行期上下文作用域链。同样,try-catch语句中的catch子句也有同样的影响,因此也要小心使用。
- 嵌套的对象成员会明显影响性能,尽量少用。
- 属性或方法在原型链中的位置越深,访问它的速度也越慢。
- 通常来说,你可以通过把常用的对象成员、数组元素、跨域变量保存在局部变量中来改善JavaScript性能,因为局部变量访问速度更快。
- for、while和do-while循环性能特性相似,所以没有一种循环类型明显快于或慢于其他类型。
- 避免使用for-in循环,除非你需要遍历一个属性数量未知的对象。
- 改善循环性能的最佳方式是减少每次迭代的运算量和减少循环迭代次数。
- 通常来说,switch总是比if-else快,但并不总是最佳解决方案。
- 在判断条件较多时,使用查找表比if-else和switch更快。
- 浏览器的调用栈大小限制了递归算法在JavaScript中的应用;栈溢出错误会导致其他代码中断运行。
- 如果你遇到栈溢出错误,可将方法改为迭代算法,或使用Memoization来避免重复计算。
-
字符串操作和正则表达式编写:
- 当连接数量巨大或尺寸巨大的字符串时,数组项连接是唯一在IE7及更早版本中性能合理的方法。
- 如果不需考虑IE7及更早版本的性能,数组项连接是最慢的字符串连接方法之一。推荐使用简单的+和+=操作符替代,避免不必要的中间字符串。
- 回溯既是正则表达式匹配功能的基本组成部分,也是正则表达式的低效之源。
- 回溯失控发生在正则表达式本应快速匹配的地方,但因为某些特殊的字符串匹配动作导致运行缓慢甚至浏览器崩溃。避免这个问题的办法是:使相邻的字元互斥,避免嵌套量词对同一字符串的相同部分多次匹配,通过重复利用向前查看的原子组去除不必要的回溯。
- 提高正则表达式效率的各种技术手段会有助于正则表达式更快地匹配,并在非匹配位置上花更少的时间(参见:“更多提高正则表达式效率的方法”)
- 正则表达式并不总是完成工作的最佳工具,尤其当你只搜索字面字符串的时候。
- 尽管有许多方法可以去除字符串的首尾空白,但使用两个简单的正则表达式(一个用来去除头部空白,另一个用来去除尾部空白)来处理大量字符串内容能提供一个简洁而跨浏览器的方法。从字符串末尾开始循环向前搜索第一个非空白字符,或者将此技术同正则表达式结合起来,会提供一个更好的替代方案,它很少受到字符串长度影响。
- 访问和操作DOM
- 最小化DOM访问次数,尽可能在JavaScript端处理
- 如果需要多次访问某个DOM节点,请使用局部变量存储它的引用
- 小心处理HTML集合,因为它实时连系着底层文档。把集合的长度缓存到一个变量中,并在迭代中使用它。如果需要经常操作集合,建议把它拷贝到一个数组中。
- 如果可能的话,使用速度更快的API,比如querySelectorAll()和firstElementChild。
- 要留意重绘和重排;批量修改样式时,“离线”操作DOM 树,使用缓存,并减少访问布局信息的次数。
- 动画中使用绝对定位,使用拖放代理。
- 使用事件委托来减少事件处理器的数量。
- UI线程
- 任何JavaScript 任务都不应当执行超过100毫秒。过长的运行时间会导致UI 更新出现明显的延迟,从而对用户体验产生负面影响。
- JavaScript 运行期间,浏览器响应用户交互的行为存在差异。无论如何,JavaScript 长时间运行将导致用户体验变得混乱和脱节。
- 定时器可用来安排代码延迟执行,它使得你可以把长时间运行脚本分解成一系列的小任务。
- Web workers 是新版浏览器支持的特性,它允许你在UI 线程外部执行JavaScript代码,从而避免锁定UI。
6.编程实践
- 通过避免使用eval()和Function()构造器来避免双重求值带来的性能消耗。同样的,给setTimeout()和setInterval()传递函数而不是字符串作为参数。
- 尽量使用直接量创建对象和数组。直接量的创建和初始化都比非直接量形式要快。
- 避免做重复的工作。当需要检测浏览器时,可使用延迟加载或条件预加载。
- 在进行数学计算时,考虑使用直接操作数字的二进制形式的位运算。
- JavaScript的原生方法总会比你写的任何代码都要快。尽量使用原生方法。
二、Vue项目优化
当项目工程化之后,框架已经帮我们处理了很多优化。按以上原理,可以从打包优化、渲染优化、项目整体优化三个角度展开。这里就不展开写了,因为网络上总结的好文已经太多太多。
三、总结
俗话说,万变不离其宗。框架再怎么发展,原理还是不变的。《高性能JavaScript》这本2010年出版的书,真是经典。框架和库是为开发人员服务的,就像应用市场中的应用是为人们提供生活上的便利。所以框架以固定方式帮我们处理了很多问题,让我们专注于业务逻辑的开发。而明白框架的原理和设计思想,能更好的让我们使用框架,当框架出现不符合我们需求情况的时候,我们就能以其恰当的方式完成改动。一切都在掌控之中!
附录
- 书籍:尼古拉斯·泽拉斯.高性能JavaScript
- 文章:2023 前端性能优化清单