代码优化

  • JS开销和如何缩短解析时间【为什么我的JS运行慢】
    • js开销在哪里
    • 解决方案
    • 减少主线程工作量
    • Progressive Bootstrapping(渐进式启动)
  • 配合V8 有效优化代码【路走对了才能快】
    • V8编译原理
    • 抽象语法树
    • V8优化机制
  • 函数优化
    • 函数的解析方式
  • 对象优化【JS对象避坑地图】
    • 对象优化可以做哪些
  • HTML优化
    • 借助工具
  • CSS对性能的影响
    • 样式计算开销
    • CSS优化

JS开销和如何缩短解析时间【为什么我的JS运行慢】

js开销在哪里

  • 加载
  • 解析&编译
  • 执行

资源大小相同的情况下,js的开销更高

170kb的js和jpg,通过网络加载的时间一致(加载过程只是大小影响),但是js后面要经历编译解析(2s)、执行(1.5s),jpg要经历解码(64.9ms)、图片绘制到页面(0.028s)

summary里可以看到解析的是哪个脚本
Bottom-Up自下而上,是一个拆分,里面具体做了哪些事,耗时多久,解析好事1757ms,垃圾回收时间61.5ms,编译1.6ms

对于一个网站而言,总共的网络加载过程中,压缩后1.4M的js在整个网络加载耗时中占1/3

解决方案

  • Code splitting代码拆分,按需加载
    当前访问路径需要哪些资源加载哪些资源,不需要的进行延迟,或者访问需要它的页面时再加载
  • Tree shaking代码减重
    不用的代码摇掉

减少主线程工作量

  • 避免长任务
  • 避免超过1kb的行间脚本
    浏览器引擎没办法对行间脚本进行有效优化,行间脚本越大,解析消耗的时间就越长
  • 使用rAF和rAC进行时间调度

Progressive Bootstrapping(渐进式启动)

可见不可交互 vs 最小可交互资源集

配合V8 有效优化代码【路走对了才能快】

V8编译原理

V8是chrome浏览器的js引擎,它是目前做得最好、效率最高的js引擎,后台nodejs也是采用v8引擎

浏览器或者v8引擎拿到js脚本后,首先进行parse it(解析),将它翻译成抽象语法树(AST),所有的编程语言都有这样的过程,要把文本识别成字符,然后把重要信息提取出来,变成一些节点,存储在一定的数据结构里,再利用数据结构理解你写的内容是什么寓意,理解什么寓意是Interpreter(解释器)要做的事,在把代码编程机器码去运行之前,编译器会进行优化工作,这个编译器是Optimising Compiler(有优化功能的编译器),有时它做的自动优化工作并不一定合适,所以再运行时,发现所做优化不合适时,会发生逆优化、反优化的过程,把刚刚做的优化去掉,这样的情况反而会降低我们的效率,所以在代码层面做的优化是尽量满足优化的条件,它怎么做优化,我们按照它期望的代码去写,回避造成它反优化过程的代码
下面写个逆优化代码,运行在node环境下

const {performance, PerformanceObserver} = require('perf_hooks');const add = (a, b) => a+b;const num1 = 1;
const num2 = 2;performance.mark('start');for(let i = 0; i < 10000000; i++) {add(num1, num2);
}add(num1, 's');for(let i = 0; i < 10000000; i++) {add(num1, num2);
}performance.mark('end');const observer = new PerformanceObserver((list) => {console.log(list.getEntries()[0]);
})
observer.observe({entryTypes: ['measure']});performance.measure('测量1', 'start', 'end');


代码运行时间大概是54ms,注释add(num1, ‘s’);再运行,代码运行时间减少到19ms,看代码就是add函数,虽然调了很多次,但是参数很稳定,每次都是两个数相加,这两个数都不变,所以在编译过程中会对这个函数进行优化,如果打开add(num1, ‘s’),在某次执行函数时,发现参数类型发生变化,运行时不能用已经做过优化的逻辑了,要把刚做的优化撤销掉,这样会带来一定的延迟

如果想进一步了解v8到底对什么做了优化,对什么做了反优化,可以利用node的两个参数(trace-opt,trace-deopt)

抽象语法树

  • 源码=>抽象语法树=>字节码Bytecode=>机器码
  • 编译过程会进行优化
  • 运行时可能发生反优化

V8优化机制

  • 脚本流
    脚本正常情况下要先下载再进行解析再执行的过程,chrome在这边做了优化,在下载过程中也同时进行解析的话可以加快这个过程,下载一个脚本,当它超过30kb时,它就认为已经足够大,可以对这30kb的内容先进行解析,会单独开一个线程去给这段代码进行解析,等整个都加载完成时,再进行解析时,效率就大大提高了,因为把前面的部分已经解析过了,把所有解析的内容合并下,然后就可以进行执行,这是流式处理的一个特点
  • 字节码缓存
    如果有些东西使用频率比较高,可以把它进行缓存,再次进行访问时就可以加快访问,源码被翻译成字节码之后,发现有一些不仅在当前页面有使用,在其他页面也会使用的片段,把这些片段对应的字节码缓存起来,在其他页面再次访问相同逻辑时,直接从缓存去取它,不需要再进行翻译的过程,这样效率就大大提高
  • 懒解析
    主要对于函数而言,虽然声明了这个函数,不一定马上会用它,默认情况下会进行懒解析,先不去解析函数内部的逻辑,当我真正要用时我再去解析函数声明的函数体,不需要解析的话也不需要为它去创建语法树,进一步而言,在我们堆的内存空间里也不用为这个函数进行内存的分配,这样对性能是极大的提升

函数优化

函数的解析方式

  • lazy parsing懒解析 vs eager parsing饥饿解析
    不能否认懒解析作为默认的解析方式,可以极大提高js的整体效率,但是在现实中,有时还是希望函数立即执行,这样会有什么问题?如果我们的函数是立即执行的,在刚开始声明的时候,默认对它进行懒解析,但是我们尤发现它要立即执行,于是又进行快速的饥饿解析,这样就对同一个函数先进行懒解析再进行饥饿解析,导致效率降低了一半,所以需要一种方式告诉我们的解析器,我这个函数需要立即执行,你现在就对它进行饥饿解析,接下来我们看下在代码里怎么告诉解析器我的函数是需要进行eager parsing的,也进行性能的前后对比
// test.js
export default () => {const add = (a, b) => a*b; // lazy parsing// const add = ((a, b) => a*b); // eager parsingconst num1 = 1;const num2 = 2;add(num1, num2);
}
// 默认情况下当它读到add函数声明(const add = (a, b) => a*b;)时,是使用lazy parsing,只记下来这个声明,并不对它进行解析,到add(num1, num2)遇到函数调用时,真正的对函数进行解析,再进行调用,我们自己在写逻辑时,自己是清楚的,很快就要调用函数,所以在我们声明时,我们需要它解析声明的同时,能把函数的函数体也进行解析,调用的时候效率反而会更高,const add = ((a, b) => a*b); 就可以进行eager parsing
// App.jsx
import test from './test';constructor(props) {super(props);// this.calculatePi(1500); // 测试密集计算对性能的影响test(); // 测试函数lazy parsing, eager parsing}
// webpack.config.js
entry: {app: './src/index.jsx',test: './src/test.js' // 测试函数lazy parsing, eager parsing},output: {path: `${__dirname}/build`,filename: '[name].bundle.js'},

npm start运行,分析发现test.bundle.js的解析时间大概是0.4ms

  • 利用Optimize.js优化初次加载时间
    js会进行压缩,当用工具进行压缩时,实际上又会帮我们把eager parsing的括号去掉,会导致我们本来想做的事没办法通知到解析器,为了处理和解决这个问题,有人做了Optimize.js这个工具,帮助我们在这种情况下把括号添加回来

对象优化【JS对象避坑地图】

对象优化可以做哪些

做这些优化的根据是迎合V8引擎进行解析,把你的代码进行优化,它也是用代码写的,它所做的优化其实也是代码实现的一些规则,如果我们写的代码可以迎合这些规则,就可以帮你去优化,代码效率可以得到提升

  • 以相同顺序初始化对象成员,避免隐藏类的调整
    js是动态、弱类型语言,写的时候不会声明和强调它变量的类型,但是对于编辑器而言,实际上还是需要知道确定的类型,在解析时,它根据自己的推断,它会给这些变量赋一个具体的类型,它有多达21种的类型,我们管这些类型叫隐藏类型(hidden class),之后它所做的优化都是基于hidden class进行的
class RectArea { // HC0constructor(l, w) {this.l = l; // HC1this.w = w; // HC2}
}
// 当声明了矩形面积类之后,会创建第一个hidden class(HC0),
const rect1 = new RectArea(3,4); // 创建了隐藏类HC0, HC1, HC2
// 对于编辑器而言,它会做相关的优化,你在接下来再创建的时候,还能按照这个顺序做,那么就可以复用这三个隐藏类,所做的优化可以被重用
const rect2 = new RectArea(5,6); // 相同的对象结构,可复用之前的所有隐藏类const car1 = {color: 'red'}; // HC0,car1声明对象的时候附带会创建一个隐藏类型
car1.seats = 4; // HC1,追加个属性再创建个隐藏类型const car2 = {seats: 2}; // 没有可复用的隐藏类,创建HC2,car2声明时,HC0的属性是关于color的属性,car2声明的是关于seats的属性,所以没办法复用,只能再创建个HC2;HC1不是只包含seats的属性,是包含了color和seats两个属性,也会强调顺序,隐藏类型底层会以描述的数组进行存储,数组里会去强调所有属性声明的顺序,或者说索引,索引的位置
car2.color = 'blue'; // 没有可复用的隐藏类,创建HC3
  • 实例化后避免添加新属性
const car1 = {color: 'red'}; // In-object 属性,对象创建就带有的属性
car1.seats = 4; // Normal/Fast 属性,存储在property store里,需要通过描述数组间接查找,没有对象本身的属性查找得快
  • 尽量使用Array代替array-like对象
    array-like对象:js里都有一个arguments这样的对象,它包含了函数参数变量的信息,本身是一个对象,但是可以通过索引去访问里面的属性,它还有length的属性,像是一个数组,但它又不是数组,不具备数组带的一些方法,比如说foreach
    如果本身真的是数组,v8引擎会对这个数组进行极大性能的优化,只是array-like的话,它做不了这些事情,在调用array方法时,通过间接的手段可以达到遍历array-like对象,但是效率没有在真实数组上高
Array.prototype.forEach.call(arrObj, (value, index) => { // 不如在真实数组上效率高console.log(`${ index }:${ value }`);
});// 将类数组先转成数组,再进行遍历,转换也是有代价的,这个开销与后面性能优化对比怎么样?v8做了实践,得出结论:将类数组先转成数组,再进行遍历比不转换直接使用效率要高,所以我们也最好遵循它的要求
const arr = Array.prototype.slice.call(arrObj, 0); // 转换的代价比影响优化小
arr.forEach((value, index) => {console.log(`${ index }:${ value }`);
});
  • 避免读取超过数组的长度
    这是讲越界的问题,js里不容易发现这越界问题,越界了也不一定报错
    越界比较的话会造成沿原型链额外的查找,这个能相差到6倍
function foo(array) {for (let i = 0; i <= array.length; i++) { // 越界比较,正常是<,这边是<=,超过边界的值也会比较进来,if(array[i] > 1000) { // 1.造成array[3]的值undefined与数进行比较 2.数组本身也是一个对象,在数组对象里找不到要的属性之后,会沿原型链向上查找,会造成额外的开销console.log(array[i]); // 这个数据是无效的,会造成业务上无效、出错}    }
}
// [10,100,1000]
  • 避免元素类型转换
    对于编辑器而言,实际上是有类型的
const array = [3, 2, 1]; // PACKED_SMI_ELEMENTS,满的整型元素array.push(4.4); // PACKED_DOUBLE_ELEMENTS,之前对数组具体到PACKED_SMI_ELEMENTS类型所做的优化全都无效,需要对数组类型进行一次更改,变成PACKED_DOUBLE_ELEMENTS类型,会造成额外的开销,编辑器效率就不高了


类型越具体,编辑器能做的优化就越多,如果变得越通用,能做的优化余地就越少
可以去v8官方看看技术博客,会经常更新它们的优化方案,我们如果可以不断配合他们的优化方案,可以让我们代码的效率不断提高

HTML优化

html优化空间比较小,html大小在整个页面所有资源里占比比较小,但是也不能忽视,优化工作要做到极致,即使1kb也不能放弃,

在html里,有很多没有用的空间,还有一些可以省略的元素,就类似上图中的企鹅群,大家可以再挤一挤,挤在一起就可以达到优化的目的

  • 减少iframes使用
    额外添加了文档,需要加载的过程,也会阻碍父文档的加载过程,如果它加载不完成,父文档本身的onload事件就不会触发,一直等着它,在iframe里创建的元素,比在父文档创建同样的元素,开销要高出很多;非要用iframe的话,可以做个延时加载,不要一上来就加载iframe,声明一个iframe,在父文档加载完成之后,再拿到iframe,再对src赋值,让它做加载,达到延迟的目的,不会影响刚开始页面的加载过程

  • 压缩空白符
    编程的时候,为了方便阅读,会留空白或者空行,这些空白符也是占空间的,最后打包时要把空白符去掉

  • 避免节点深层级嵌套
    嵌套越深消耗越高,节点越多最后生成dom树占有内存会比较高,有个遍历,嵌套越深遍历就越慢

  • 避免使用table布局
    table布局本身有很多问题,使用起来没有那么灵活,造成的开销非常大,同样实现一种布局的方式,用table布局开发和维护起来,相对而言都更麻烦

  • 删除注释
    把无效内容去掉,减少大小

  • CSS&Javascript尽量外链
    CSS和Javascript直接写在行间,会造成html文档过大,对于引擎来说,后续也不好做优化,css和js有时确实要做在行间,这个和偷懒写在行间是两码事

  • 删除元素默认属性
    本身默认那个值,没有必要写出来,写出来就添加了额外的字符,要通过网络传送给客户端,这就是一些浪费

    head里有很多meta,每个meta要清楚对应的作用,没有用的不要写上去,都是浪费
    css通过外部css进行引入

    body部分多使用html5的语义标签,方便浏览器理解你写的内容是什么,可以进行相关的优化

    有一些元素,前面有open tag,后面有closing tag,并不是所有元素需要closing tag,比如img、li

    考虑可访问性,video,浏览器支持或者不支持,还有支持的视频格式都要进行考虑
    js要放在body的尾部进行加载,为了防止影响dom的加载,js是阻塞的,如果开始就进行加载,它的加载解析就会影响后面dom的加载

借助工具

html-minifier

CSS对性能的影响

样式计算开销

  • 利用DevTools测量样式计算开销

复杂度计算,降低计算的复杂度,对元素进行定义样式,尽量定义单一的样式类去描述它的样式,尽量不要使用过于复杂的伪类,多层级联,去锁定这个元素进行样式描述
css解析的原则是自右向左去读,先会找出最具体的元素,把所有的a全都找出来,再根据#box进行过滤,再进行过滤,再进行过滤,直到把所有受到影响的元素全都过滤出来,然后运用这个样式,随着浏览器解析不断进步,现在这种复杂度的计算已经不是最主要的问题

CSS优化

  • 降低CSS对渲染的阻塞
    由于CSS对渲染的阻塞是无法进行避免的,所以我们从两个角度进行优化:1尽量早的完成css的下载,尽早的进行解析;2降低css的大小,首次加载时,只加载当前路径或者首屏有用的css,用不到的进行推迟加载,把影响降到最低
  • 利用GPU进行完成动画
  • 使用contain属性

    从上图可以看出,没有使用contain布局消耗的时间大概是56.89ms,使用之后可以降低到0.04ms,这是一个非常大的优化

    contain有多个值,layout是其中一个,是现在目前主流浏览器支持比较好的值,作用也比较大
    上图是新闻的一个展示页,如果想在第一条内容里插入其他一些内容,对于我们关键渲染路径而言,浏览器并不能知道你插入的东西会不会影响到其他元素的布局,这个时候它就需要对这个页面上的元素进行重新的检查,重新的计算,开销很大,这里有将近10000条的新闻,将近10000个元素要受到影响,如何降低影响?因为我们只是想在第一条里去插入一个东西,后面这些元素本身是不会受到影响的,形状和大小都不会变,这个时候我们就用到contain,contain是开发者和浏览器进行沟通的一个属性,通过contain:layout告诉浏览器,相当于你可以把它看成一个盒子,盒子里所有的子元素和盒子外面的元素之间没有任何布局上的关系,也就是说里面无论我怎么变化不会影响外面,外面怎么变化也不会影响盒子里面,这样浏览器就非常清楚了,盒子里面的元素如果有任何的变化,我会单独的处理,不需要管理页面上其他的部分,这样我们就可以大大减少重新去进行回流或者布局时的计算,这就是contain:layout的作用
  • 使用font-display属性,可以帮助我们让我们的文字更早的显示在页面上,同时可以适当减轻文字闪动的问题

(四)代码优化 (快来看看怎样写出真正高性能的代码)相关推荐

  1. 如何利用Citespace和vosviewer既快又好地写出高质量的论文及快速锁定热点和重点文献进行可视化分析?

    基于Citespace和vosviewer文献计量学可视化SCI论文高效写作方法 CiteSpace是什么? 简单来说,它一款通过将国内外文献进行可视化分析来帮助你了解一门学科前世今生的软件. 面对成 ...

  2. 如何写出优雅的 Golang 代码

    Go 语言是一门简单.易学的编程语言,对于有编程背景的工程师来说,学习 Go 语言并写出能够运行的代码并不是一件困难的事情,对于之前有过其他语言经验的开发者来说,写什么语言都像自己学过的语言其实是有问 ...

  3. 如何一本正经地写出别人无法维护的代码?

    作者 | 阿木 责编 | 伍杏玲 出品 | 程序人生(ID:coder_life) 编写除了自己没人能看懂的代码,是一种怎样的体验? 下面由作为资深挖坑程序员的我,手把手教大家这是怎么做到的?如果各位 ...

  4. 作为一名程序员,怎样写出高效简洁的代码?

    前言: Hello大家好,我是Dream .经常有朋友问我,自己写的代码太乱,虽然功能都能实现但是写的并不简洁,让人一眼看上去就会很难受,那如何去写出简洁优雅的代码呢?那今天我就来和大家分享一下一些写 ...

  5. 如何写出优质干净的代码,这6个技巧你不能错过!

    编写干净的代码并不是一件容易的事情,这需要尝试不同的技巧和实践. 作为一名开发者,编写一手干净的代码很重要. 先列举出编写干净代码的一些好处,再提出6个技巧用于编写干净代码,供开发者进行参考学习. 开 ...

  6. 【整洁之道】如何写出更整洁的代码(上)

    如何写出更整洁的代码 代码整洁之道不是银弹,不会立竿见影的带来收益. 没有任何犀利的武功招式,只有一些我个人异常推崇的代码整洁之道的内功心法.它不会直接有效的提高你写代码的能力与速度,但是对于程序员的 ...

  7. 如何写出更优雅的代码——编程范式简述

    <如何写出更优雅的代码--编程范式简述>源站链接,阅读体检更佳! 什么是程序? 1976年,瑞士计算机科学家,Algol W,Modula,Oberon 和 Pascal 语言的设计师 N ...

  8. 20条开发规范,写出诗一样的代码

    文章目录 简介 命名 1.命名的长度,多长合适 2.利用上下文简化命名 2.1 利用类class上下文简化命名 2.2 利用函数function上下文简化命名 3. 命名可读.可搜索 3.1 命名可读 ...

  9. 程序员如何写出更好的代码

    Martin Thompson是Java Champion称号获得者,同时也是一名高性能计算科学家.他说,为了写出更好的代码,程序员需要运用基本设计原则,阅读已有代码.在QCon London 201 ...

最新文章

  1. Delphi XE5 for Android (十)
  2. laravel5.6 数组传递到前端
  3. hdu1530 最大团简单题目
  4. angular 拦截器
  5. 六十三、栈在括号匹配和表达式求值中的应用
  6. httpclient 设置超时时间_面试官:技术选型,HttpClient还是OkHttp?
  7. 数据结构——交换左右子树
  8. 【高并发】掌握JUC中的阻塞队列
  9. Windows 10 May 2020 中 WSL 与 WSL2 的性能比较
  10. gpg: no valid OpenPGP data found. 解决办法
  11. Python中,.join()的用法
  12. SQL Server 2008 卸载报错
  13. ev3编程 越野机器人_乐高机器人EV3,让机器人动起来
  14. 工具说明书 - 单词发音及根据发音查单词
  15. 计算机重启发出响声怎么办,电脑不断响提示音怎么办
  16. Redis之SDS数据结构
  17. js教程实践(JS基础)
  18. 使用Bootstrap制作网页主界面、增加界面
  19. 点任务栏不切换窗口_如何使您的任务栏按钮始终切换到最后一个活动窗口
  20. c语言遍历 json字符串,全面详解c语言使用cJSON解析JSON字符

热门文章

  1. 小技巧:让linux程序在后台运行
  2. Java3y文章目录导航
  3. AD 修改密码返回错误 Set-ADAccountPassword : 从服务器返回了一个参照。
  4. java-基础-ArrayList剖析
  5. [整理+原创]ubuntu Thunderbird Mail设置自动提醒
  6. Android--使用XMLPull解析xml
  7. Linux系统时间与RTC时间【转】
  8. Oracle——17概要文件
  9. Google 历年笔试面试30题
  10. Angularjs1.x 中的 service,factory,provider,constant,value