1000字范文,内容丰富有趣,学习的好帮手!
1000字范文 > (四)代码优化 (快来看看怎样写出真正高性能的代码)

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

时间:2020-10-10 05:32:37

相关推荐

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

代码优化

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.jsexport 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.jsximport test from './test';constructor(props) {super(props);// this.calculatePi(1500); // 测试密集计算对性能的影响test(); // 测试函数lazy parsing, eager parsing}

// webpack.config.jsentry: {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 {// HC0 constructor(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属性,可以帮助我们让我们的文字更早的显示在页面上,同时可以适当减轻文字闪动的问题

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。