WdBly Blog

懂事、有趣、保持理智

周维的个人Blog

懂事、有趣、保持理智

站点概览

周维 | Jim

603927378@qq.com

推荐阅读

优化web性能

web性能优化

有些大家可能清楚了解有过实践、有些点可能也是第一次听说, 有些是我们系统存在的问题,有些是做的比较很好的,有些是现有系统不太会遇到的问题。

1、什么是性能优化?

  • 性能优化所指向的对象是一个完整的系统

  • 简而言之,就是在不影响系统运行正确性的前提下,使之运行地更快,完成特定功能所需的时间更短。

image.png

2、为什么需要性能优化?

对于web系统来说,首先了解到,不好的性能可能会导致的一些问题

  • 响应时间增加,容易流失用户
  • 动画执行卡顿,交互效率低下
  • 内存CPU消耗过大,导致网页卡顿

一个性能良好的网站,能更好的留住用户

3、性能优化的一般原则

  • 1、从数据分析而不是猜测
    这是性能优化的第一原则,当我们怀疑性能有问题的时候,应该通过测试、日志、profillig来分析出哪里有问题,有的放矢,而不是凭感觉、撞运气。

  • 2、不要过早优化
    vczh: 过早指的不是在开发过程的早期,而是在还没弄清楚需求未来的变化的走向的时候。你的优化不仅可能导致你无法很好地实现新的需求,而且你对优化的预期的猜测有可能还是错的,导致实际上你除了把代码变复杂以外什么都没得到。

Donald Knuth: 程序员们浪费了大量的时间去思考、担心程序中非关键部分的性能,如果考虑整体代码的debug和维护的时间,则这些性能优化的开销是得不偿失的。对于约 97% 的小优化点,我们应该忽略它们。而对于剩下的关键的 3%,我们则不能放弃优化的机会。

总结两个论点:1: 过早优化是基于不完善或猜想的需求所做的扩展、优化设计。2: 过早优化是在开发的起始阶段就把非关键部分的性能优化作为一个重要的任务来考虑,在没有实际数据指标的基础上,为了性能提前做的些盲目优化工作。

image.png

  • 3、不要过度优化
    性能优化的目标是追求合适的性价比。过度优化是指为了优化性能,过度增加系统复杂度和维护成本,或使得开发周期变长。虽然可能性能上带来了一定的提升,但是和过度优化而导致的这些缺点来比,这么做显而易见是得不偿失的。

image.png

  • 4、深入理解业务
    代码是服务于业务的,也许是服务于最终用户,也许是服务于其他程序员。不了解业务,很难理解系统的流程,很难找出系统设计的不足之处。

image.png

  • 5、找到合适的衡量指标
    性能优化是一个长期的行为,所以需要固定衡量指标、环境,这样才能客观反映性能的实际情况,也能展现出优化的效果。

image.png

4、性能优化的通用方法

  • 1、缓存
    缓存的本质是加速访问,访问的数据要么是其他数据的副本,为了让数据离用户更近;要么是之前的计算结果,为了避免重复计算。

  • 2、并发
    一个人干完活要10天,分配十个人干,一天做完,这是广义上的并发。

  • 3、惰性
    将计算推迟到必需的时刻,这样很可能避免了多余的计算,甚至根本不用计算。

  • 4、批量,合并
    通过批量合并减少I/O,提高吞吐量

  • 5、更高效实现
    同一个算法,肯定会有不同的实现,那么就会有不同的性能;有的实现可能是时间换空间,有的实现可能是空间换时间,有的可能是时间空间都有所提升。

  • 6、更小的解空间
    缩小解空间的意思是,在一个更小的数据范围内进行计算,而不是遍历全部数据。

  • 7、充分利用合适的设计模式优化业务逻辑

  • 8、使用好的编程技巧提升代码运行效率

5、系统的性能问题是如何产生的?

系统的性能问题的产生是伴随这系统的整个生命周期,不同阶段产生的性能问题不一,优化的方案也不竟相同。

5.1、需求阶段

需求是为了解决某个问题,问题是本质,需求是解决问题的手段。那么需求是否能否真正的解决问题,程序员也得自己去思考。

产品经理(特别是知道一点技术的产品经理)的某个需求可能只是某个问题的解决方案,他认为这个方法可以解决他的问题,于是把解决方案当成了需求,而不是真正的问题。

需求分析对性能优化有什么帮助呢,第一,为了达到同样的目的,解决同样问题,也许可以有性能更优(消耗更小)的办法。这种优化是无损的,即不改变需求本质的同时,又能达到性能优化的效果;第二种情况,有损的优化,即在不明显影响用户的体验,稍微修改需求、放宽条件,就能大大解决性能问题。PM退步一小步,程序前进一大步。

image.png

image.png

5.2、设计阶段

架构设计约束了系统的扩展、技术选型决定了代码实现。好的设计是优化的关键,有些由于设计上带来的性能问题甚至需要将系统推倒重来。

image.png

设计阶段的技术选型的意义重大,由于选择的语言框架等底层适用性产生的性能问题将很难有完美解决方案

  • eg1、缓存失效造成雪崩问题
    在高并发,大量数据访问到程序时,若是缓存失效,则大量请求同时涌入数据库,导致数据库长时排队,造成的雪崩问题。比如应用刚刚启动时或者缓存过期的差时。

使用事件队列的设计解决

// 1、原始写法 var select = function (callback) { db.select("SQL", function (results) { callback(results); }); }; // 2、使用状态判断解决, 这样会有什么问题? var status = "ready"; var select = function (callback) { if (status === "ready") { status = "pending"; db.select("SQL", function (results) { status = "ready"; callback(results); }); } }; // 使用事件队列 var proxy = new events.EventEmitter(); var status = "ready"; var select = function (callback) { proxy.once("selected", callback); if (status === "ready") { status = "pending"; db.select("SQL", function (results) { proxy.emit("selected", results); status = "ready"; }); } };

第二种种情景下,连续地多次调用select()时,只有第一次调用是生效的,后续的select()是没有数据服务的。

利用事件队列解决缓存崩溃其实是利用合适的设计模式优化业务。

  • eg2、opweb设计
    image.png

现有的加载流程是一个串式的,如果有其它启动是就要加载的数据,需要排在串式设计后,造成时间损耗。

可以利用性能优化的通用方法中的并发思想解决现有问题。

设计阶段还会有比如多异步场景的协调问题等等。

5.3、编码阶段

不好的编码规范会导致一些性能问题,接下来上干货

5.3.1、高效率的JS代码

一、数据存取

  • 1、使用局部变量:在函数中读取局部变量是最快的,读取全局变量是最慢的,因为通过作用域链解析标识符需要开销,并且作用域链越长,开销越大。如果在函数中需要多次引用全局变量也可定义局部变量等于全局变量来优化。
var global = 0; function() { // 在函数内部定义一个变量, 后续直接访问局部变量即可 var temp = global; temp++;temp--; console.log(temp); }

利用了缓存全局变量的副本,让数据离用户更近的思想进行优化

  • 2、JS读取属性和变量的效率如何?
var t1 = +new Date() var element = {name: 'jim'} var name = element.name; ... var t2 = +new Date() t2 - t1 // 读取变量、属性 for(var i = 0; i < 10000000; i++) { var n = element.name // 44 // var n = name // 1800 } // 读取属性 var obj = {a: {b: {c: {d: {e: {f: 1}}}}}, b: 1} for(var i = 0; i < 10000000; i++) { var n =obj.a.b.c.d.e.f // 26 // var n = obj.b // 31 }

通过测试可以看出,读取对象属性的速度要大于读取变量的速度,当然这个差距不大。

当然为了更好的可读性,如果多次操作对象深层次属性,可以考虑先将其缓存为一个变量,这遵循了不要过度优化原则

// bi后台代码 let data = await config.athena .getQueryExecution({ QueryExecutionId }) .promise(); if (data.QueryExecution.Status.State === "SUCCEEDED") { retry = config.retry; resolve(data); } else if (data.QueryExecution.Status.State === "FAILED") { reject(data.QueryExecution.Status.StateChangeReason); } else { setTimeout(() => { keepCheckingRecursively(); }, retry); } // 使用对象解构 缓存深层属性 const data = await config.athena .getQueryExecution({ QueryExecutionId }) .promise() const { state, StateChangeReason } = data.QueryExecution.Status const SUCCEEDED = 'SUCCEEDED' const FAILED = 'FAILED' if (state === SUCCEEDED) { retry = config.retry; resolve(data); } else if (state === FAILED) { reject(StateChangeReason); } else { setTimeout(() => { keepCheckingRecursively(); }, retry); }

二、算法和流程控制

  • 1、循环
    • for-in用于变量对象key,其循环效率低于for、while, 速度只有for循环的1/7,通常用于对象的遍历。
    • forEach等基于函数的迭代:forEach会基于数组的每一项调用函数,和for循环相比速度如何?
var list = new Array(10000000).fill(1) // 千万级 var t1 = +new Date() var n ... var t2 = +new Date() t2 - t1 // 1、一个普通的for循环 for(let i = 0; i < list.length; i++){ n = list[i] // chrome 150、node 10 } // 2、for in for(i in list){ n = list[i] // chrome 3000、node 1343 } // 3、forEach list.forEach((item, i)=> { n = list[i] // chrome 150、node 130 }) // 4、map list.forEach((item, i)=> { n = list[i] // chrome 150、node 133 }) // 5、优化版本1, 缓存list.length,因为length值是不变的 for(let i = 0, len = list.length; i < len; i++){ n = list[i] // chrome 50、node 10 } // 6、优化版本2,倒序循环,把i的值作为控制条件,去掉 i < len的比较消耗 for(let i = list.length; i--;){ n = list[i] // chrome 40、node 10 }

bi code
image.png

image.png

image.png

使用缓存length必须满足在循环中length不变。这项优化利用了缓存思想里的缓存计算结果,避免重复计算

使用倒序循环必须满足0是循环的结束点。这利用了使用好的编程技巧提升代码运行效率

顺便提一下,箭头函数如果只是return了一个表达式,可以省略return语句,好处是更简洁美观。

// jsx {reports.map(item => (<Report />))} // js const list = reports.map(item => ({ item }))
  • 2、条件语句
// 1、 将最可能出现的条件前置以减少判断的次数 var num = Math.random() * 100 if (num < 1) { ... } else if (num < 2) { ... } else { ... }

将最有可能的条件前置,后面的条件判断大概率不执行,其实也是利用了惰性的思想,避免了多余的计算。

// 2、将扁平化的判断转换为嵌套式的判断 var num = Math.floor(Math.random() * 10) if (num === 1) {} else if (num === 2) {} ... else if (num === 10) {} // 通过2分的思想嵌套 if (num < 5) { if (num < 3) { }else { } } else { if (num > 7) { }else { } }

将扁平化的判断转换为嵌套式的解法,满足缩小解空间的一般优化方法。

在极限情况下扁平化的判断优于嵌套化。

// 3、通过表查找法替换if/else或switch。所谓的表查找法是将条件和值作为key/value方式存储,查找效率O(1) // bi code switch (zone_ava) { case 'ap': where = `tar_ava='${tar_ava}' and channel_ava='default' and zone=1 and dt='${dt}'` break; case 'eu': where = `tar_ava='${tar_ava}' and channel_ava='default' and zone=2 and dt='${dt}'` break; case 'us': where = `tar_ava='${tar_ava}' and channel_ava='default' and zone=3 and dt='${dt}'` break; case 'default': where = `tar_ava='${tar_ava}' and channel_ava='default' and zone=0 and dt='${dt}'` break; default: break; } // 修改后 const resMap = { 'ap': `tar_ava='${tar_ava}' and channel_ava='default' and zone=1 and dt='${dt}'`, 'eu': `tar_ava='${tar_ava}' and channel_ava='default' and zone=2 and dt='${dt}'`, 'us': `tar_ava='${tar_ava}' and channel_ava='default' and zone=3 and dt='${dt}'`, 'default': `tar_ava='${tar_ava}' and channel_ava='default' and zone=0 and dt='${dt}'` } where = resMap[zone_ava]

表查找法替换if/else或switch其实是一种更高效的实现优化
BI getDailyData.ts / columnsSQLFun方法应用了这种方法。

  • 3、递归
function A(){ return 1; } function B(){ A(); } function C(){ B(); } C();

Js执行栈中除了当前执行函数的栈帧,还保存着调用其函数的栈帧,在A释放前,执行栈中保存了A、B、C的栈帧,过长的调用栈帧在Js中会导致一个栈溢出的错误。

由此可知,在递归调用中,很容易出现栈溢出的错误。

// 尾递归优化 function fibonacci (n) { if ( n <= 1 ) return 1 return fibonacci(n - 1) + fibonacci(n - 2); } fibonacci(100) //卡死 // 尾递归优化 function fibonacci2 (n , ac1 = 1 , ac2 = 1) { if( n <= 1 ) {return ac2}; return fibonacci2(n - 1, ac2, ac1 + ac2); } fibonacci2(1000) // 7.0330367711422765e+208

要使用尾递归优化需要满足两个条件,一是严格模式、二是满足尾调用

尾调用是函数的最后一步调用一个函数,由于调用函数是最后一步操作,所以不需要保留外层函数的调用帧,所以在C调用B后就会释放。

三、Ajax

当前团队内部已经有较好的Ajax封装,这一块过只提一个。

  • 使用图片向服务器发送信息:具体方式为,新建一个图片对象new Image(),并将其src属性设置为服务器的urlsrc = url + params,服务器会接受到数据并保存,使用这种方式的消耗是非常小的。 参考

四、编程实践

  • 1、避免重复工作:很多次的代码重构其实都在在消除重复,尽量在开发阶段处理好,不要想着重构是再来优化。
// bi code const prefixFunc = () => { let { client, server } = global.awsTableInfo let { global_ava, cn_lt } = global.awsPartitionsInfoEnd //客户端存储前缀 client.tableName.map(dtaName => { global_ava.zone_ava.map(zone => { let str = { own:"client", bucketName:client.bucketName, database:client.database, event_ava:dtaName.replace("event_ava_", ""), pro_ava:"cave_online", tar_ava:"global", zone_ava:zone } arrPrefix.push(str) }) cn_lt.zone_ava.map(zone => { let str = { own:"client", bucketName:client.bucketName, database:client.database, event_ava:dtaName.replace("event_ava_", ""), pro_ava:"cave_online", tar_ava:"cn_lt", zone_ava:zone } arrPrefix.push(str) }) }) } // 修改点 // let => const、 map => forEach、 str => object // 从两个级别抽出公共的属性。 const prefixFunc = () => { const { client, server } = global.awsTableInfo const { global_ava, cn_lt } = global.awsPartitionsInfoEnd const prefix = { bucketName: client.bucketName, database: client.database, pro_ava: "cave_online", } //客户端存储前缀 client.tableName.forEach(dtaName => { prefix.own = 'client' prefix.event_ava = dtaName.replace("event_ava_", "") global_ava.zone_ava.forEach(zone => { prefix.tar_ava = 'global' prefix.zone_ava = zone arrPrefix.push(prefix) }) cn_lt.zone_ava.forEach(zone => { prefix.tar_ava = 'cn_lt' prefix.zone_ava = zone arrPrefix.push(prefix) }) }) }

关于重复,比较难找的是功能意义上的重复工作,比如两个类似的函数做了类似的工作等等,需要在开发设计阶段注意。

  • 2、利用Web Worker创建多线程处理长耗时的计算操作,防止UI被卡住。
// 主线程 var worker = new Worker('http://www.avalon.com/work.js'); worker.postMessage('start'); worker.onmessage = function ({ data }) { console.log('Received message ' + data); // doSomething } // worker线程 self.addEventListener('message', function ({ cmd }) { if (cmd === 'start') { const res = 0 for(let i = 100000; i--;) { // 做一些复制计算 res++ } // 将计算结果返回主线程 self.postMessage(res); } }, false);

Web Worker创建多线程计算是利用了并发的思想优化耗时操作。

5.3.2、框架和构建

一、框架-React

  • 为列表设置唯一key :设置唯一key可以让diff算法在对比同一级元素变动时有更好的表现(不用频繁的删除和添加元素,而是移动元素位置)

  • 使用useCallback缓存方法的引用,避免子组件进行无意义的重复渲染

// bi code SettingRender const [selectTimezoneKey, setSelectTimezoneKey] = useState<string>('L') const apiRequest = async () => { console.log(selectTimezoneKey) ... } <SubmitButton> <Button type="primary" onClick={() => { apiRequest() }} > 提交修改 </Button> </SubmitButton> // 使用useCallback缓存apiRequest, 避免SubmitButton组件无意义重新渲染 const apiRequest = useCallback( async () => console.log(selectTimezoneKey), [selectTimezoneKey] ); <SubmitButton> <Button type="primary" onClick={apiRequest} > 提交修改 </Button> </SubmitButton>
  • 使用useMemo缓存子组件的引用
// bi code Board const [name,setName] = useState('') ... <CopyBoard name={name} /> // 使用useMemo缓存组件 const Child = useMemo( () => <CopyBoard name={name} />, [name] ); return ( <Child /> )

2、3点都是我们现在可以实际提升的点。

二、webpack 构建

  • 1、按需加载
    image.png
// 1、 .babelrc { ... "plugins": [ ... ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }, { "libraryName": "lodash", "libraryDirectory": "fp" }] ] } // 2、使用 import { Button } from 'antd' // 正确 import * as antd from 'antd' // 还是会加载全部 // 3、结果: 文件大小减少了1M

按需加载的实质是将组件或函数单独发布,在使用时单独导入。import bar from 'echarts/lib/chart/bar'

由于这样写太长了,不好记,所以有babel-plugin-import插件帮我们处理

利用babel-plugin-import插件将模块的导入转换为按需加载,antd, antd-mobile, lodash, material-ui等库可以支持使用此插件。

部分库如echarts需要手动按需加载、自己封装的npm包则需要配置按需加载。!

webpack构建的按需加载满足了惰性的优化思想

  • 2、文件缓存
// webpack output output: { filename: '[name]/bundle-[hash].js', path: distDir, publicPath: '../', }, // 修改后 output: { filename: '[name]/bundle-[chunkHash:6].js', path: distDir, publicPath: '../', },

使用hash,只要文件有改动,打包后所有文件的hash值都发生了改变。
image.png

image.png

修改过为chunkHash后打包
image.png
image.png

使用chunkHash开启更小粒度的缓存,一些怎么变动的库文件则能被客户端长期缓存提升网站加载速度。

5.3.3、传输和渲染

一、传输

  • DNS预解析:使用场景通常是网站中包含了大量的外部资源,
  • HttpDns防劫持:image.png
  • CDN加速资源访问 -已启用
    • 物理层的硬件优化:更快的硬盘读取,更高的网络带宽
    • 网络层的寻址优化:寻找距离最近的资源,依赖于网络层的路由协议寻址算法
    • 传输层的优化:TCP的慢启动和流量控制可以用于避免网络拥塞
    • 应用层的缓存优化:资源合理设置缓存
  • 启用HTTP2.0 -已启用
    • 多路复用:一个域名维护一个TCP链接,http请求以流的方式传输,实现资源并行下载
    • 头部压缩:使用静态表和动态表压缩http的头部信息
  • 图片系统:能支持图片格式转换、裁剪以适应不同客户端

二、渲染

  • translate3d:开启translate3d可以让GPU参与加速动画渲染,这是一种欺骗浏览器的hack方法,让浏览器认为即将渲染3D动画,其实元素根本没有在z轴运动。
  • will-change:will-change是css3新增的属性,和translate3d类似,是一种加速动画渲染的方法,js在异步事件中触发浏览器大量绘制界面时,浏览器往往是没有准备的被动使用cpu去计算页面渲染,而will-change可以提前告知浏览器此元素的渲染需要gpu参与高速渲染。用法:will-change: transform, will-change: contents
  • 使用高性能动画属性:如transform、opacity等,减少使用box-shadow等消耗较大的属性动画。
  • 减少回流的动画:元素的某些属性在发生变化后会导致浏览器大面积重绘页面,视觉上反应为动画卡顿。
  • js的执行和渲染互斥:js在执行期间将主线程中的渲染操作收集起来,并在本轮事件循环结束(微任务执行完毕)后执行所有渲染操作,但是如果在js中获取布局信息则会打乱这里过程,迫使浏览器将暂存的所有渲染操作优先执行,然后获取最新的渲染状态,如获取元素的offsetTop、clientTop等。
  • requestAnimationFrame:requestAnimationFrame是H5新增的api,传统的js实现动画在setInterval中实现,setInterval中的代码并不是严格意义上的定时执行,有可能造成性能问题,requestAnimationFrame则充分利用了屏幕的刷新机制,可以使用此API代替setInterval。

6、如何找到系统性能瓶颈?

  • 1、chrome开发者工具

演示网站

  • 2、webpack bundle分析工具

演示地址

总结

衡量代码质量的标准是可读性、可维护性、可扩展性,但性能优化有可能会违背这些特性,比如为了屏蔽实现细节与使用方式,我们会可能会加入接口层(虚拟层),这样可读性、可维护性、可扩展性会好很多,但是额外增加了一层函数调用,如果这个地方调用频繁,那么也是一笔开销;

这种有损代码质量的优化,应该放到最后,不得已而为之,同时写清楚注释与文档。

为了追求可扩展性,我们经常会引入一些设计模式,如状态模式、策略模式、模板方法、装饰器模式等,但这些模式不一定是性能友好的。所以,为了性能,我们可能写出一些反模式的、定制化的、不那么优雅的代码,这些代码其实是脆弱的,需求的一点点变动,对代码逻辑可能有至关重要的影响,所以还是回到前面所说,不要过早优化,不要过度优化。

提交

全部评论0

暂时没有评论...