【前台性能优化】高性能JavaScript读书笔记

作者 : 开心源码 本文共7822个字,预计阅读时间需要20分钟 发布时间: 2022-05-12 共185人阅读

封皮一隅

曾经看过一篇文章,有一句话这样说:

只有在大学的图书馆里,你才能真正赚回你交的学费。

临近毕业,还想再去图书馆多转转。偶然在架子上发现了这本书,一看作者是写大名鼎鼎的红宝书的人,就很感兴趣。再者,最近用 JavaScript 刷 LeetCode 发现,提交显示 JavaScript 要比 Go 语言或者 Python 有更大的时间和内存消耗,也使我把理解 JavaScript 内存机制和性能优化提上了日程。

本书尽管有部分章节涉及到的问题有肯定年代感,比方最后一章[工具],因为前台技术的快速迭代和浏览器的不断支持下,已经不适用了。但是这本书的前面章节详细地从js运行、访问、代码结构优化、异步编程等多方面讲解了 JavaScript 优化的策略,解答了我刷题时的疑惑,也让我认识到之前秋招面试的时候遇到的少量坑,还是有许多需要引起重视的知识体系需要不断扩容。

总之,这本书是对前台性能优化基础知识的一种补充掌握,推荐有项目经验并希望提升Web应用性能的前台开发人员阅读。

前台性能优化

下面是我认知的前台性能优化的策略,本书主要着手 JavaScript 优化开展阐述。

  • JavaScript优化
  • 非核心代码异步加载
  • 浏览器缓存
  • 使用CDN
  • DNS预解析
  • 优化资源
  • 清除不必要的依赖

本文目录


高性能JavaScript

早期,IE浏览器的JS引擎基于“静态垃圾回收机制(Static Garbage Collection)”,该引擎监视内存中固定数量的对象来确定何时进行垃圾回收。随着Web应用的日益发展,JS引擎吃不消了。

尽管其余浏览器有着更加完善的GC和更好的性能,但大多数都是使用JS解释器来执行。

这也正解释了开篇刷 LeetCode 题时的困惑,解释型代码为什么没有编译型代码快?

由于,解释型代码必需经历把代码转换成计算机指令的过程。无论解释器多么智能,都会带来少量性能的消耗。

解释型代码转换
而编译器已经有了各种各样的优化,可以基于词法分析去判断代码想实现什么,产生完成任务的运行最快的机器码。解释器很少有这样的优化,往往代码怎样写就怎样被执行。

2008年,JS引擎收获最大的一次性能更新,该引擎的研发代号为V8。V8是一款为 JavaScript 打造的实时(JIT)编译引擎,它把 JavaScript 代码转化为机器码来执行。紧接着其余浏览器也优化了JS引擎,这些只是编译器层面的优化,代码的性能仍然需要开发人员关注。

编译器层面的优化

一、浏览器中的 JavaScript

浏览器中js代码的执行可能会阻塞浏览器的其余进程,下边列出了几点辣手的问题以及优化方式。

  1. 脚本阻塞:将<script>标签放在页面底部,</body>闭合标签之前。

  2. 推迟时间:

    1. 内嵌<script>不紧跟<link>标签
    2. 运用打包工具,合并js文件
  3. 无阻塞加载js:关键是在window对象的load事件触发后再下载脚本

    1. 使用<script>标签的defer属性
      注意:defer属性仅当src属性公告时才生效

    2. 动态脚本加载:使用动态创立的<script>元从来下载并执行代码
      注意:需要通过侦听事件,跟踪并确保脚本下载完成并准备就绪
      优势是跨浏览器兼容性和易用,也是最通用的无阻塞加载的策略。

    3. 使用 XHR 对象下载 JavaScript 代码并注入页面中

      局限性:JavaScript 文件必需和所请求的页面同域,不适用大型Web项目。

无阻碍脚本加载工具:YUI3、LazyLoad、LABjs

通过以上策略,可以极大地提高JavaScript的Web应用的性能。
此外,还有少量策略例如:减少js文件的大小、限制HTTP请求数。这两点策略,随着Web应用的日益复杂,可行性也随之降低,也不是做的越极致效果越好,需要实际情况具体分析。

二、数据存储的位置

数据存储的位置关系到数据的检索速度,直接影响代码执行的效率。JavaScript 有以下四种基本的数据存储位置:

  1. 字面量:值的记法,包括:字符串、数字、布尔值、对象、数组、函数、正则表达式,还有特殊的 null 和 undefined 值
  2. 本地变量:使用 var/let/const 关键字定义的数据存储单元
  3. 数组元素:以数字为索引,存储在 JavaScript 数组对象内部
  4. 对象成员:以字符串作为索引,存储在 JavaScript 对象内部

标识符解析的性能

在函数的执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取或者存储数据。该过程的搜索执行环境是作用域链,这个搜索过程会影响性能。

注意:总的趋势是,标识符所在位置越深,它的读写速度越慢。若采用优化过的 JavaScript 引擎的浏览器性能损失会大大减少。

原型链和嵌套成员也遵从此关系。

注意作用域链的改变

可以在执行时改变作用域链,影响性能的语句:

  1. with 语句:会导致一个新的变量对象被置于作用域链的首位,造成访问特定对象的属性非常快,而访问局部变量则变慢。
    建议:弃用

  2. try-catch语句中的 catch 子句:会把异常对象推入一个变量对象并置于作用域的首位。
    建议:将错误委托给一个函数解决

闭包

闭包的[[scope]]属性包含了与执行环境作用域链相同的对象的引用,同时会影响内存开销和执行速度,应小心使用闭包。

策略

可以通过把常用的数组元素、跨域变量保存在局部变量中来改善性能。

这种策略不推荐用于对象的成员方法,会改变this的值。

三、DOM 编程

浏览器中通常会把 DOM 和 JavaScript 独立实现,所以访问DOM元素消耗很大。

策略:减少访问DOM的次数,把运算留给ECMAScript一端。

innerHTML 比照 DOM 方法

旧版浏览器中,使用innerHTML会更快少量。在基于 WebKit 内核的新版浏览器中,用DOM略胜一筹。

策略:根据可读性、稳固性、团队习惯、代码风格来综合决定。

节点克隆

节点克隆element.cloneNode()比创立新元素document.createElement更有效率,但不显著。

HTML集合

返回值是集合的方法:

  • document.getElementByName()
  • document.getElementByClassName()
  • Document.getElementByTagName()

返回值是集合的属性:

  • document.images
  • document.links
  • document.forms
  • document.forms[0].elements

HTML集合是包含DOM节点引用的类数组对象。和数组的区别是没有push和slice方法,有length属性和数字索引的方式访问元素。

HTML集合低效之源:假定实时态 assumed to be live

策略:

  1. 把集合的长度缓存到一个局部变量中,在循环条件的退出语句中使用该变量。

  2. 使用数组拷贝。

    function toArr() {    for (var i = 0, arr = [], len = coll.length; i < len; i++) {        arr[i] = coll[i];    }    return arr;}

遍历DOM

属性名被替代的属性
childrenchildNodes
childElementCountchildNodes.length
firstElementChildfirstChild
lastElementChildlastChild
nextElementSiblingnextSibling
previousElementSiblingpreviousSibling

选择器API

queryAelectorAll()firstElementChild()方法使用CSS选择器作为参数并返回一个NodeList,不会返回HTML集合。适合解决大量组合查询。

重排和重绘

在浏览器的渲染过程中,浏览器会在下载完页面所有组件之后,解析并生成两个数据结构:

  • DOM Tree(DOM树)
  • Render Tree(渲染树)

一旦上述两种结构构建完成,浏览器就开始绘制(paint)页面元素。

注:对重排和重绘的了解是非常必要的

重排 Reflow

定义:当DOM结构的变化影响了元素的几何属性,浏览器需要根据样式来重新计算元素出现的位置。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。

触发Reflow的条件:

  • 增加或者删除可见的DOM元素
  • 元素位置改变:如,增加动画效果
  • 元素尺寸改变:如,改变边框宽高、内外边距等
  • 内容改变:如,改变段落文字行数、图片替换等
  • 浏览器Resize窗口(手机端不会出现)
  • 修改默认字体
  • 页面渲染器初始化

特别的:当滚动条出现时,会触发整个页面的重排

重绘 Repaint

定义:完成重排后,浏览器会根据渲染树重新绘制受影响的部分到屏幕中。

不是所有的DOM变化都会影响几何属性,例如改变一个元素的背景色只会发生一次重绘。

特别的,要注意分析改变所影响的阶段是重排还是重绘。

综上,重排和重绘都是昂贵的操作,会导致Web应用反应迟钝。所以,应该尽可能减少这类过程的发生。

渲染树的变化的排队和刷新

浏览器会通过队列化批量执行来优化重排过程。

以下获取布局的操作会导致队列刷新:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()

修改样式时,应避免以上属性。

策略:不要在布局信息改变时操作它。

最小化重排和重绘

策略:

  1. 合并屡次对DOM和样式的修改,而后一次解决掉。(n -> 1)

    如:cssText属性,className属性等。

  2. 尽量减少offsets等布局信息的获取次数,方法是获取一次起始位置的值,在动画循环中,直接使用变量。

  3. 让元素脱离动画流:拖放代理商

    • 使用绝对定位页面上的动画元素,将其脱离文档流。
    • 让元素动起来,这时会临时覆盖部分页面,只会发生小规模重绘。
    • 当动画结束时恢复定位,从而只会下移一次文档的其余元素。
  4. 在元素很多时,避免使用:hover

批量修改DOM

关键:“离线”操作DOM树,使用缓存,减少访问布局信息的次数。
策略:

  1. 使元素脱离文档流
    • 隐藏元素(display:none),应用修改,重新显示。
    • 使用文档片段在当前DOM之外构建一个子树(document.createDocumentFragment()),再把它拷贝回文档。(推荐)
    • 将原始元素拷贝到一个脱离文档流的节点中,修改副本,完成后再替换原始元素。
  2. 对其应用多重改变
  3. 把元素带回文档中

事件委托

之前写过一篇<了解DOM事件解决程序和事件委托>的文章,涉及事件模式的基本概念、事件流、事件委托的实现等的阐述,假如大家对以上概念有所遗忘,欢迎点击链接查看原文。

每绑定一个事件解决器都会加重页面负担、延长执行时间、消耗更多的内存(由于浏览器会跟踪每个事件解决器)。

一个优雅的策略就是利用事件委托

可以将冗长的浏览器兼容性代码移入可重用的类库:

  • 访问事件对象,判断事件源
  • 取消文档树中的冒泡
  • 阻止默认动作

四、算法和流程控制

大部分性能问题的来源是低效的算法或者工具编写出的糟糕代码

循环

代码执行的大部分消耗在循环

JS循环的类型:

  1. for循环

    for语句执行流程

注:for循环初始化中var语句会创立一个函数级的变量,应尽可能使用ES6中的let语句定义循环级变量。

  1. while 循环:和for相似,是最简单的前测循环
  2. do-while 循环:唯一的后测循环,循环体至少运行一次。
  3. for-in 循环:枚举任何对象的属性名key
  4. for-of 循环:ES6新特性,枚举任何对象的值value

拓展知识:for-in 和 for-of 区别

所返回的属性:

  1. 对象的实例属性
  2. 从原型链中继承的属性

循环性能:for-in 显著慢

因为每次操作会同时搜索实例和原型属性,查询散列键,会产生更多开销。所以,除了明确需要迭代一个属性数量未知的对象,其余情况应避免使用for-in。

若其余循环的性能都差不多,其实只有两个因素可以提升整体性能:

  1. 减少每次迭代的工作量:限制循环中的耗时操作总数
    • 最小化属性查找
      关键:减少对象成员及数组项的查找次数
      策略:只查找一次属性,并把值存到一个局部变量中。例如:var len = items.length;
    • 倒序循环
      通常,数组项的顺序与所要执行的任务无关。倒序循环是编程语言中一种通用的性能优化方式。

当循环复杂度为O(n)时,减少每次迭代的工作量是最有效的。当复杂度大于O(n),建议着重减少迭代次数。

  1. 减少迭代的次数
    达夫设施(Duff’s Device):循环体开展技术,一次迭代中实际执行了屡次迭代的操作。

迭代数超过1000,使用 Duff’s Device 的执行效率将显著提升。

基于函数的迭代 forEach() 显著慢

起因:对每个数组项调用外部方法所带来的额外开销。

条件语句

if-else 比照 switch
基于测试条件的数量选择:条件数量越大,越倾向于使用switch,易读性强且速度快。

大多数语言对 switch 语句的实现都采用了 branch table(分支表)索引进行优化。

优化 if-else

  1. 最小化到达正确分支前所需条件判断的次数
    策略:条件语句按照从大概率到小概率的顺序排列
  2. 把 if-else 组织成一系列嵌套的if-else 语句
    策略:二分法把值域分成一系列区间,逐渐缩小范围。
    适用范围:有多个值域需要测试。
    查找表
    当条件语句数量很大或者有大量散离值需要测试时,使用数组普通对象构建查找表访问数据比较快。

优点:当单个键和单个值之间存在逻辑映射时,随着候选值添加,几乎不产生额外开销。

递归

传统算法的递归实现:阶层函数
潜在问题;

  1. 假死
    策略:为了安全在浏览器工作,可以迭代和Memoization结合使用。
  2. 浏览器调用栈大小限制 Call stack size limites
    当超过最大调用栈容量时,浏览器会报错,可以用try-catch定位。
    策略:ES6中使用尾递归就不会发生栈溢出,相对节省性能。

五、字符串和正则表达式

字符串连接

方法示例
The + operatorstr = “a” + “b” + “c”;
The += operatorstr = “a”; str += “b”; str += “c”;
array.join()str = [“a”, “b”, “c”].join(“”);
string.concat()str = “a”; str = str.concat(“b”,”c”);
转义字符””在每一行的最后,都加上转义斜线 \
使用es6模版字符串使用键盘1左边的字符 ` 拼接

字符串连接优化

str += 'zhu' + 'yue'; //2个以上的字符串拼接,会在内存中产生临时字符串str = str + 'zhu' + 'yue'; //推荐,直接附加内容给str,提速10%~40% 

浏览器合并字符串时分配的方法:除IE外,为表达式左侧的字符串分配更多的内存,而后简单地将第二个字符串拷贝至它的末尾。

正则表达式优化

基本概念:正则表达式
注意避免:回溯失控

使用正则表达式和倒序循环可以简单实现trim方法,去首尾空白。

优化正则表达式的策略:

  1. 具体化分隔符之间的字符串匹配模式
  2. 使用预查和反向引用的模拟原子组
  3. 避免嵌套量词与回溯失控
  4. 关注如何让匹配更快失败
  5. 以简单必须的字元开始
  6. 使用量词模式,使它们后面的字元互斥
  7. 较少分支数量,缩小分支范围
  8. 把正则表达式赋值给变量并重用
  9. 化繁为简

何时不使用正则表达式

  1. 在特定位置上提取并检查字符串的值:slice、substr、substring
  2. 查找特定字符串位置,或者者判断它们能否存在:indexOf、lastIndexOf

六、快速响应的客户界面

Web Workers 引入了一个接口,能使代码运行且不占用浏览器线程的时间。

Worker的运行环境:

  • 一个 navigator 对象,只包括四个属性:appName、appVersion、user Agent 和 platform
  • 一个 location 对象(与window.location 相同,不过所有属性都是只读的)
  • 一个 importScripts() 方法,用来加载 Worker 所用到的外部 JavaScript 文件
  • 所有的 ECMAScript 对象
  • XMLHTTPRequest 构造器
  • setTimeout() 和 setInterval() 方法
  • 一个 close() 方法,可以立即中止 Worker 运行。

Web Workers 实际应用

Web Workers 适用于:

  1. 解决纯数据
  2. 与浏览器无关的长时间运行脚本
  3. 编码/解码大字符串
  4. 复杂数学运算,如:图像和视频
  5. 大数组排序

例子:解析一个很大的JSON字符串

var worker = new Worker("jsonParser.js");//数据就位时,调用事件解决器worker.onmessage = function (event) {    //JSON结构被回传回来    var jsonData = event.data;    //使用JSON结构    evaluateData(jsonData);};//传入要解析的大段JSON字符串worker.postMessage(jsonText);

jsonParser.js文件中 Worker 中负责解析JSON的代码:

//当JSON数据存在时,该事件解决器会被调用self.onmessage = function (event) {    //JSON字符串由event.data传入    var jsonText = event.data;    //解析    var jsonData = JSON.parse(jsonText);    //回传结果    self.postMessage(jsonData);}

超过100毫秒的解决过程,应该考虑 Worker 方案。

七、AJAX

常常使用XMLHttpRequest(XHR)、Dynamic script tag insertion、multipart XHR技术向服务器请求数据。

XMLHttpRequest:可以参考之前写过的文章 用原生JS封装AJAX
Dynamic script tag insertion:可以跨域请求数据
multipart XHR:将服务端资源打包成商定好的字符串分割的长字符串,并发送到用户端。

数据格式:JSON

此章节优化主要是有效的利用浏览器缓存,还有本章没有提及的现在逐步开始流行的 fetch API也值得探讨。

八、编程实践

  1. 避免双重求值,即在JavaScript代码中执行另一段JavaScript代码,是JavaScript运行期性能优化的关键。
  2. 使用 Object/Array 直接量
  3. 通过推迟加载和条件预加载,避免重复工作
  4. 使用语言中速度快的部分,如:位操作(& | ^ ~)、原生方法

九、构建并部署高性能JavaScript应用

构建和部署的过程对基于js的web应用的性能有着巨大影响。这个过程中最重要的步骤有:

  1. 使用Gzip合并、压缩js文件,能够减少约70%的体积。
  2. 通过正确设置HTTP响应头来缓存js文件,通过向文件名添加时间戳来避免缓存问题。
  3. 使用CDN提供js文件;CDN不仅可以提升性能,也帮助管理文件的压缩与缓存。
  4. 使用Webpack构建。

拓展:前台构建工具的发展

十、工具

主要分析方面:

  1. 性能分析
  2. 网络分析

总结

JavaScript 在不断发展和扩充它的边界,我们也要不断学习大量的优化技术和方法。当把这些策略应用在项目中时,将会看到性能的显著提升,这也就是细节决定成败

最后,培养和保持良好的开发习惯,对于个人发展和团队合作都是很有必要的,推荐阅读《高性能JavaScript》这本小薄书。??

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 【前台性能优化】高性能JavaScript读书笔记

发表回复