写于 2017.02.16
入手《高性能 JavaScript》一周后, 终于断断续续看完了. 简要说说感受, 就是这本书非常薄, 非常容易看, 认真看的话其实两三个小时就能翻一遍了. 这篇文章也是作为一篇阅读笔记, 用来记录我在阅读过程中的一些理解与感悟.
乍一看上去, 这本书里面有相当一部分内容是非常旧的, 很多优化手段在如今高速的网络环境以及先进的浏览器的加持下, 视乎已经失去了必要性. 然而作为一个有洁癖的人, 是无法允许自己的代码 "大概差不多就好", 而且我也相信任何一个有追求的人都希望自己的作品是精益求精的, 所以这本书仍然有非常大的学习意义. 抛开主观, 在阅读一些优秀开源库时, 看到别人的某些代码非常不理解, 在看完这本书以后回想起来才发出感慨, 原来人家这么做的目的是为了提升性能.
全书共分为十章内容, 以下将按照书本章节的顺序, 来逐一撰写我的阅读笔记.
一, 加载和执行
把 JS 放在</body > 结束标签之前而不是 < head></head > 标签内部能够避免浏览器阻塞, 提升用户体验, 已经算是一个常识. 这个常识的背后, 涉及到了浏览器单进程的概念.
事实上, 多数浏览器使用单一进程来处理用户界面 (UI) 刷新和 JavaScript 脚本执行, 所以同一时刻只能做一件事.
这里说的用户界面刷新, 指的是我们 "所能看到的 UI" 变化(比如点击一个按钮, 会出现按钮被按下去的效果). 换句话来说, 处理 UI 就无法处理 JavaScript, 反之亦然. 所以如果一份运行时间很长的 JS 脚本放在页面顶端, 会阻塞之后页面的下载和渲染, 给用户的感觉就是 "页面一片空白卡死不会动".
虽然现在网速和浏览器的效率已经得到了巨大的提升, 但随着移动端的兴起以及前端框架如 vue,React 的大量使用, 这个问题还是非常值得我们注意的.
二, 数据存取
首先对于数据的存取, 有以下这么一句关键:
每一个 JS 函数都会带有一个叫做 [[Scope]] 的内部属性, 也就是该函数的作用域链, 它决定了哪些数据能被函数访问.
书上详细介绍了作用域链, 执行上下文, 活动对象, 全局对象, 闭包等概念, 在这里就不进行复述了. 用我自己理解的话来说, 就是一个函数若要使用一个变量, 它会从最近的地方, 也就是定义在函数内部的局部变量里面去找; 若没有找到, 则往更远处的全局变量 (或者上一级作用域) 里面去找. 恰恰是这个 "找" 的过程, 产生了性能的问题. 书上使用了 "解析标识符" 来表述 "找" 这个动作, 而 JS 性能恰恰是随着解析标识符深度的增加而降低, 所以在最佳实践里, 往往是通过把一个较深的变量赋值给一个局部变量, 在函数内部直接调用这个局部变量来提升性能.
说完变量, 就到了方法. 在 JS 中一切皆对象, 然而 JS 的对象是基于原型而来, 这就引出了一个原型链的概念. 与前文关于解析标识符的原理类似, 要调用一个对象中的方法, 首先会从这个对象实例中查找, 若找不到, 则会沿着其原型链一步一步由近到远地往上查找, 其性能也是随之下降的.
另外, 书上也讨论到了关于 "嵌套成员" 的问题. 比如 Windows.location.href, 它会先找到 Windows 对象, 然后查找嵌套于内的 location 对象, 再找到这里面的 href 属性, 前前后后套了多层, 在性能上也有着一定的花销. 所以在实际的编码过程中, 我们更多时候会面对的往往是这种嵌套成员的问题, 时刻记得缓存对象成员的值, 在执行完毕后利用 cacheObj = null 的方式释放缓存, 可以有效地提高性能, 如下例子:
- // bad
- document.querySelector('.xxx').style.margin = 10 + 'px'
- document.querySelector('.xxx').style.padding = 10 + 'px'
- document.querySelector('.xxx').style.color = 'pink'
- // good
- let xxxStyle = document.querySelector('.xxx').style
- xxxStyle.margin = 10 + 'px'
- xxxStyle.padding = 10 + 'px'
- xxxStyle.color = 'pink'
- xxxStyle = null
三, 浏览器中的 DOM
这一章节详细介绍了关于 dom 操作的一系列问题. 首先要明确一个知识点就是 dom 操作是具有 "天生就慢" 的问题. 为什么会如此呢? 因为在浏览器里面, 处理 html 和 JS 是两套不同的机制, 他们通过接口来进行联系的. 引用书中的原话, 就是可以把 HTML 和 JS 理解为两座岛, 他们之间需要一座桥来进行沟通, 而过桥则会产生时间与成本上面的开销, 也因此引起了性能的问题. 这一章节通过分析不同的 dom 操作函数, 来综合对比了各种方法的速度.
dom 操作往往容易引起浏览器的重绘与重排. 重排, 指的是页面的布局和几何属性改变时所发生的事情; 重绘, 是指把 dom 元素绘制到屏幕上面的过程.
会造成性能问题的, 往往来自于重排, 因为浏览器需要重新计算页面所有元素的大小与位置, 然后把它们安置在正确的地方. 所以, 要提升页面的性能, 很重要的一个举措就是避免页面的重排.
值得注意的是, 并非只有在修改页面元素的大小和位置的时候才会引发重排, 在获取的时候浏览器也会出发重排, 以返回正确的值.
然而很多时候我们不得不直接操作 dom, 尽管它们会引起重排和重绘. 书上给出了几个方案, 都能有效提升性能. 其实方法和上文关于 JS 缓存局部变量的方法类似, 也是通过缓存的机制, 减少对于 dom 元素属性的查找, 以及批量修改变量再一次性更新 dom 的办法去减少查询与修改.
除此以外, 让元素脱离文档流也是一个很好的方法. 因为元素一旦脱离文档流, 它对其他元素的影响几乎为零, 性能的损耗就能够有效局限于一个较小的范围.
讲完重排与重绘, 往 dom 元素上绑定事件也是引起性能问题的元凶. 利用浏览器自带的冒泡或捕获机制, 可以通过事件委托的方式减少事件处理器的数量, 从而把性能优化得更好.
四, 算法和流程控制
这一章首先分析了几种循环类型, 结论是只有 for-in 循环的性能最慢, 因为每次迭代都会同事搜索实例或原型属性, 导致其性能只有其它类型速度的 1/7. 循环在代码中非常常见, 既然无法避免, 则需要通过尽量减少循环次数, 减轻每次循环的工作量的方式提升性能.
对于条件语句 if else 或者 switch, 其性能在现实中并没有太大区别, 关键是要正确处理语义化的需求. 有的时候也可以使用查表法进行.
对于递归算法, 最好的提升性能方法是缓存上次执行的结果, 在下一次递归的时候直接引用而非从头开始计算.
五, 字符串和正则表达式
TODO...... (对于正则表达式还没有特别熟悉, 这一章跳着看了)
六, 快速响应的用户界面
前面五章都是针对 JS 原生的语法分析性能问题, 从这一章开始分析针对用户界面的可感知性能问题.
由于浏览器是单线程运作的, 在处理 UI 事件的时候无法处理 JS 事件, 反之亦然, 所以对于耗时过长的 JS 任务来说, 可以使用定时器的方法使其让出线程控制权, 让浏览器优先处理 UI 事件以提升用户体验.
html5 新增的 web worker 允许多开线程, 意味着耗时较长, 性能损耗较大的 JS 任务可以放到 Web worker 中进行, 而无需阻塞浏览器 UI 线程的执行. 值得注意的是, Web worker 无法使用浏览器相关的资源, 所以无法用以进行 dom 操作等.
七, Ajax
Ajax 技术已经是如今的主流技术, 在这里就无需赘述了. 书上关于其性能优化的内容, 多集中在浏览器资源缓存上. 如果能够有效利用浏览器的缓存机制, 可以大大减少与服务端的交互, 提升性能.
书上没有提及的是现在逐渐开始流行的 fetch API, 关于这方面的性能的问题也值得我们研究.
其他
剩下的内容都是一些编程实践, 代码优化等等.
在如今的前端开发领域中, 上线的代码一般都会经过代码合并, 压缩, 服务端开启 gzip 等工作. 随着 http2 的发展, 网页的性能更会得到提高, 可能传统 "文件合并" 这一工作会逐渐被摒弃. 另外 http2 的服务端推送也能极大地提升页面加载速度, 这部分内容在我另外一篇文章《深入研究: HTTP2 的真正性能到底如何》有详细研究, 有兴趣的读者可以去看看.
《高性能 JavaScript》这本书非常精致, 内容也非常丰富. 这篇读书笔记仅仅作为首次阅读草草而作的读书笔记, 对于书中内容的理解或多或少都会有失偏颇, 如果发现有错漏或者更好的理解方式, 欢迎留言和我交流~
来源: https://juejin.im/post/5c3c73d86fb9a049c965ec35