已通过作者授权

前言

之前我也研究过很多性能相关的文档和博客,发现现在的性能相关的文章 90% 都是之前有过的东西,但是目前的性能优化只能做到如今的样子了吗?

很显然,肯定不是的,技术本来就是个逐渐进步的过程,但是现在更多的是把当前的内容去翻来覆去的卷,我表示卷不动了,所以我准备寻找新的出路了

想法的诞生

其实我们现在的性能优化的检测及性能优化的方案已经有了很多了,从开发到用户体验的各个角度来说,都有不同的检测和处理方案,目前市面上流传最多的就是以下这些:

  • 开发阶段(公共变量、公共样式、组件提取、数据处理算法、影响页面渲染速度和用户响应的使用worker(元素除外)等)

  • 打包构建(gzip 压缩、去log、去 sourcemap、按需引入、按需加载、图片样式合并、减少打包时间和打包体积、添加缓存等)

  • 发布阶段(CI、CD)

  • 资源优化(强缓存、协商缓存、资源预加载、异步加载、service-worker等)

当然了不止这么多东西,我只是把常用的一些东西列了一下,比如我之前写过的一个实战篇 - 如何实现和淘宝移动端一样的模块化加载 (task-silce)和 解析篇 - Task-slice实现淘宝移动端方式加载这就是在开发阶段比较细节的用户体验方面的性能优化,当然我们还可以基于 performance api 来做性能优化前的检测,这方面正好之前我也整理过部分内容性能优化篇 - Performance(工具 & api)

基于这些东西我想了想,我还是觉得性能优化做的不够细不够具体,这样有很多的弊端:

  • 伪性能优化(这样就代表着性能优化做的不够彻底)

  • 不能完全的掌握页面dom渲染相关的数据(火焰图看的太复杂,没有数据化)

  • 通过 performance.mark 植入的方式,可能对于项目来说是个很大的成本,会在业务里面植入很多无效代码来做用户体验的检测,而且可能在某些情况下会影响到业务,或者业务的某些条件导致 performance.mark 无法准确抓取,这样整体来说就无法真正达到完美的目的了

这时候我就考虑要如何可以规避这些问题,还能准确的捕捉到有关当前元素的渲染时间呢,baidu、google 查了一段时间后发现了一个api好像可以解决这个问题,于是我开始入手了

timg.gif

想法的实现

实现上述想法时,我们需要梳理一下我们的需求:

  • 捕捉当前元素的渲染时间(何时开始、渲染多久、渲染位置)

  • 不把性能检测相关的代码植入到业务当中,实现上述需求

  • 捕捉到的这些信息在何处预览(在公司没有性能检测平台的情况下,我们是否要为了这种做优化相关的需求去在搭建一个性能检测平台)

  • 是否可以通过浏览器插件来展示这些数据(这样方便预览,还不影响各个方向的业务)

有了想法,剩下的就是实现即可了

捕捉当前元素的渲染时间

其实本文所述的功能,最主要就是基于这个 api 来实现的,它就是元素的 elementtiming 属性

使用方法也很简单就是给当前要检测的元素添加该属性:

   <div elementtiming="text">测试text</div>

然后在通过 PerformanceObserver对象获取相应的数据:

    const observer = new PerformanceObserver((list) => {console.log(list.getEntries())});observer.observe({ entryTypes: ["element"] });

log 里面就可以获取到 elementtiming 值为 text 的元素的相关信息:loadTime(加载时间)renderTime(渲染时间)等,这里简单介绍一下不做过多的详解,大家知道我用它做了什么就好

当然,这个 api 在该元素只包含其他元素(无文本),就不会生成 PerformanceEntry,这个问题是我在网上百度不到,但是看了 MDN 的案例发现效果不准确,在给 chromium 提了 issue后,官方回复给的答案

issue 链接:vue or react local server, new PerformanceObserver().obserbe({ entryTypes: ['element'] }) Incomplete acquisition, but build after the project unstable

这个过程是很复杂的,在了解到官方的答复后,我觉得这样的 api 它是不完善的,本来还想继续在上面链接的评论区继续讨论,但是抵不住老外手快直接把 bug 给关了

好吧,那我只能重新起一个需求出来,和他们讨论了:

issue 链接:PerformanceObserver api result not what i expected

提了这个需求后,我还等着讨论一下我的这个需求呢,但是还是很利索的告诉我这里不负责这个,让我去 WICG 那边提需求。。。

然后我就过去了:

image.png

大致的意思就是我想要的是一个完整的树状数据表,这样我可以知道我每一层数据的渲染时间和对应子级的渲染,但是老外没明白我的意思,跟我说直接获取到目标 img 或者含有文本的元素不好吗,这样还节省性能:

image.png

这明显是无法满足我的需求的,我也只能给他在详细的解释一遍了:

image.png

不知道我解释的清楚不,或者是我的需求是否也是大家需要的欢迎讨论,底部会留联系方式或者在该 issue 中讨论也行

issue 链接:PerformanceObserver api return result not what i need

好了,有关该 api 在调研和使用阶段出现的问题及我的解决办法表述先到此为止,重点是整体功能,大家会用就够

不把性能检测相关的代码植入到业务当中,实现上述需求

如题,我不想把这方面的代码嵌入到项目当中,因为如果是一个特别大的项目,我要是写一堆 performance.mark 我得写哭了,很显然这个方式是不现实的,然后我就想到是否可以通过 webpack 实现该需求呢?

那必须可以啊,解析当前的内容,然后通过拿到对应的资源去添加该属性,但是不建议直接通过内容去匹配,比如内容是这样的:

    <div class="a">this is <div class="a"> element</div>

哇嘎理工啊,如果直接把 loader 添加到 webpack 的配置当中,那么对于整个项目来说当前 loader 访问到的是当前打包文件内的所有内容,能写吗?肯定是不可以的,正则让你写到死啊

image.png

那通过 babel 解析 ast 去做渲染呢,这样可以准确的拿到对应的属性了啊,这样不就可以了吗?大概的方向对了,但是直接使用的情况下,babel 会对当前所有的内容资源进行转译,这明显不是我所需要的:

// unitl.js
export const fn1 = function() {return 1
}// component.jsexport default function() {return <div>this is <div class="a"> element </div>
}

直接只用 babel 转译的话,上述的文件都会通过 babel 转译一遍,那么这样对于我们来说并不是合理的,不能因为为了检测元素性能而导致页面构建速度变慢吗?更何况这还不是最优解

这时候我想到了一个办法,也是我目前使用的一个办法,大家可以看看是否真的是最优解,我目前是考虑到这里了:

通过 webpack plugin 在 build 前,给当前模块添加一个 loader,在当前的 loader 内去通过 babel 转译添加 elementtiming

知道了如何做就开始撸代码了,下面是调用方式:

// webpack.config.jsconst ElementRenderingWebpackPlugin = require('element-rendering-webpack-plugin')
module.exports = {plugin: [new ElementRenderingWebpackPlugin()]
}

plugin 的实现也比较简单,主要的工作是在 loader 部分:

// element-rendering-webpack-plugin.jsclass MyPlugin {apply(compiler) {compiler.hooks.compilation.tap('MyPlugin', (compilation) => {compilation.hooks.buildModule.tap('SourceMapDevToolModuleOptionsPlugin', module => {if (module.resource) {if (/(\.((j|t)sx?)$)/.test(module.resource) && !/node_modules/.test(module.resource)) {if (module.loaders) {module.loaders.push({loader: 'element-rendering-webpack-loader'})}}}})      })  }
}
module.exports = MyPlugin

「上面代码就是在 compilation 生成后,就在模块 build 前去做模块的确认,只对我自己的业务和需要的代码添加该 loader,这样就可以绕过上面直接使用 babel 方法导致的构建速度问题」

在此要对文件做一些过滤,因为是 1.0 的出版,所以还有一些东西没有完全考虑,还需要继续优化,这里提示一下暂时是不支持 vue 使用的,vue 模块的 loader 太多了,我要多做测试才敢上线,还希望大家体谅

// element-rendering-webpack-loader.jsconst parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAstSync } = require('@babel/core');
const t = require('@babel/types');
let randomSet = new Set();function UpdateAssets(asset) {let code = assettry {const ast = parser.parse(asset, {sourceType: 'module',plugins: ['flow','jsx']});traverse(ast, {JSXElement(nodePath) {if (nodePath.node.type === 'JSXElement' && nodePath.node.openingElement.name.name === 'img') {return}updateAttr(nodePath.node);}})code = transformFromAstSync(ast).code;} catch(e) {console.log(e)}return code;
}function updateAttr(node) {if (node.type === 'JSXElement') {let { openingElement, children } = node;let name = openingElement.name.name || openingElement.typelet className = openingElement.attributes.filter(attr => {if (attr.type === 'JSXSpreadAttribute') return falsereturn /class(Name)?/.test(attr.name.name)})if (className.length) {name = className[0].value.value}if (!openingElement) returnconst elementtimingList = openingElement.attributes.filter(attr => {if (attr.type !== 'JSXSpreadAttribute' && attr.name.name === 'elementtiming') {return true}})if (!elementtimingList.length) {openingElement.attributes.push(addElementttiming(name + '-' + Math.ceil(Math.random() * 100000)));}const markList = openingElement.attributes.filter(attr => {if (attr.type !== 'JSXSpreadAttribute' && attr.name.name === 'data-mark') {return true}})if (!markList.length) {openingElement.attributes.push(addMark());}children.map(childNode => updateAttr(childNode));}
}function addElementttiming(name) {return t.jsxAttribute(t.jsxIdentifier('elementtiming'), t.stringLiteral(name));
}function addMark() {let randomStatus = true;let markRandom = 0;while(randomStatus) {markRandom = Math.ceil(Math.random() * 100000);randomStatus = randomSet.has(markRandom);if (!randomStatus) {randomSet.add(markRandom);}}return t.jsxAttribute(t.jsxIdentifier('data-mark'), t.stringLiteral(markRandom + ''));
}module.exports = UpdateAssets;

这里直接上代码了,东西太多就不一行一行解释了,代码会开源,链接在底部自取慢慢看

「大概做的就是把当前跑进来的代码通过 ast 转译,拿到 ast 对象后添加 elementtiming 属性,data-mark 是用来做数据去重的」

好了,这时候最基础的 「捕获数据」「不把性能检测相关的代码植入到业务当中,实现上述需求」,那么接下来就该通过浏览器插件来展示这些数据

通过浏览器插件来展示这些数据

由于之前是真心没写过 chrome-extension ,可踩了不少坑,很多 version 2 可以用的东西 version 3 不支持

43a7d933c895d143b579154f7cf082025aaf074a.gif

这里我直接就上核心部分的代码了,剩下一些基础配置类的大家自己到时候看代码吧:

// contentScript.jschrome.runtime.onMessage.addListener(function(request) {const { type, data } = request.dataswitch(type) {case 'selectedElement':createMask(data)break;case 'cancelElement':cancelMask()break;}
})function createMask(data) {cancelMask()const div = document.createElement('div')Object.keys(data).map(styleKey => div.style[styleKey] = data[styleKey] + 'px')div.style.position = 'absolute'div.style.background = 'rgba(109, 187, 220, 0.5)'div.style.zIndex = '9999'div.id = 'mask-element'document.body.appendChild(div)
}function cancelMask() {const maskElement = document.querySelector('#mask-element')if (maskElement !== null) {document.body.removeChild(maskElement)}
}function getElementTreeData(element, elementTreeData, performanceElementTimingObj) {let children = element.childrenfor (let i = 0; i < children.length; ++i) {let childElement = children[i]let argObj = {}let nodeValue = ''let parsePerformanceElementTiming = {}if ('elementtiming' in childElement.attributes) {nodeValue = childElement.attributes.elementtiming.nodeValueargObj['elementtiming'] = trueargObj['key'] = childElement.dataset.marklet performanceElementTiming = performanceElementTimingObj[argObj['key']]if (performanceElementTiming) {parsePerformanceElementTiming = JSON.parse(JSON.stringify(performanceElementTiming))}} else {nodeValue = childElement.nodeNameargObj['key'] = Math.ceil(Math.random() * 100000)}argObj = Object.assign({}, argObj, parsePerformanceElementTiming, {interpRect: childElement.getBoundingClientRect()})if (/(NO)?SCRIPT/.test(nodeValue)) continueargObj['children'] = childElement.children.length ? getElementTreeData(childElement, [], performanceElementTimingObj) : []argObj['title'] = nodeValue.replace(/-([0-9]*)$/, '')elementTreeData.push(argObj)}return elementTreeData
}let performanceElementTimingList = []
const observer = new PerformanceObserver((list) => {let elementTree = []let performanceElementTimingObj = {}performanceElementTimingList = performanceElementTimingList.concat(list.getEntries())performanceElementTimingList.map(performanceTimingItem => {if (performanceTimingItem.element !== null) {return performanceElementTimingObj[performanceTimingItem.element.dataset.mark] = performanceTimingItem}})chrome.runtime.sendMessage({type: 'performanceTree',data: getElementTreeData(document.body, elementTree, performanceElementTimingObj)})
});
observer.observe({ entryTypes: ["element"] });

contentScriptchrome-extension 内访问页面元素的一个配置文件,当然文件名自己随便取,为了方便阅读和理解,我直接跟着官方文档的节奏走的,这里大家可以发现我上面有一个方法是 createMark 里面有创建元素和定位,这里是配合 devtools 里面的树来使用的:


// app.js
import { useState, useEffect } from 'react';
import { Tree } from 'antd';
import './App.css';
function App() {const [treeData, setTreeData] = useState([]) window.addEventListener('message', msg => {const { type, data } = msg.dataif (type === 'performanceTree') {setTreeData(data)}})useEffect(() => {}, [treeData])return (<div className="App"><TreeshowLinetitleRender={nodeData => {return (<div onMouseOver={() => { selectedElement(nodeData) }} onMouseOut={cancelElement}>{nodeData.title}{updateTime(nodeData)}</div>)}}treeData={treeData}/></div>);
}function updateTime(nodeData) {let str = ' - 'if (nodeData.renderTime) {str += Math.round(nodeData.renderTime)} else {str += '该元素下非元素外不存在文本'}return str
}function selectedElement(nodeData) {console.log('selectedElement')if (!nodeData.disabled) {postMessage({type: 'selectedElement',data: nodeData.interpRect},'*')}
}function cancelElement () {console.log('cancelElement')postMessage({type: 'cancelElement'},'*')
}export default App;

为了页面的美观度,我用了 antd 去对页面ui进行优化的,当点击某一个树的时候,会画一个框出来,标明当前元素的时间和对应的元素在哪里:

QQ20210607-174438-HD.gif

这就是最后的效果,我是直接 react 脚手架搭完直接安装的

尾声

大概的实现思路和思考的过程,基本上我都描述的差不多了,过程当中有很多次想过放弃,但是又不忍心抛弃自己之前的付出所以就坚持下来了,也算是做出来的了,但是 elementtiming api 那里那个问题,还是需要我继续研究和解决的,我会继续和 WICG 那边沟通,争取可以让它变得更好

可能有大佬看见会说这东西很简单啊,没什么值得思考地方,那我只想说dddd,我比较菜,得一步一步的学,你们轻点喷哈

代码开源了已经,欢迎大家互相讨论学习,也希望大家给点点 star,多提 issue,如果有兴趣的朋友我还希望大家一起来维护这个东西:

  • plugin: element-rendering-webpack-plugin

  • loader: element-rendering-webpack-loader

  • extension: element-rendering-extension

我是如何把性能优化的颗粒度做的更细相关推荐

  1. java性能瓶颈分析_Java性能优化技巧整理,做一个深度的程序员

    原标题:Java性能优化技巧整理,做一个深度的程序员 在我们身边是一大批的程序员,层次不一,但是放眼观,我们很容易就可以看到那些是业务型程序员,那些是有层次的程序员.注重细节,注重性能,做一个有深度的 ...

  2. Web前端性能优化,应该怎么做?

    本文将分享一些前端性能优化的常用手段,包括减少请求次数.减小资源大小.各种缓存.预处理和长连接机制,以及代码方面的性能优化等方面. base64:尤其是在移动端,小图标可以base64(webpack ...

  3. Web前端的性能优化,需要怎么做?

    文末有福利 本文将分享一些前端性能优化的常用手段,包括减少请求次数.减小资源大小.各种缓存.预处理和长连接机制,以及代码方面的性能优化等方面. base64:尤其是在移动端,小图标可以base64(w ...

  4. 前端性能优化,我们可以做哪些?

    1.雪碧图技术 这个很简单,把每个小图标都整合到一张大图上面,极大的减轻http请求数,同时能够让图片快速加载进来. 考虑到当前的5g的发展前景,以后图片不会造成加载延迟的现象. 2.浏览器渲染机制 ...

  5. 想成为精英,你的时间颗粒度够细吗?

    前言:最近在<得到>上听了一节刘润老师讲的关于时间颗粒度的课,很有启发,下面来和大家分析一下. 故事是这样的,2016年12月,一张王健林的行程安排表在网上流传.从他的行程表上可以看到,这 ...

  6. 聊聊 dotnet 7 对 bool 与字符串互转的底层性能优化

    本文也叫 跟着 Stephen Toub 大佬学性能优化系列.大家都知道在 .NET 7 有众多的性能优化,其中就包括了对布尔和字符串互转的性能优化.在对布尔和字符串的转换的性能优化上,有着非常巧妙的 ...

  7. 揭秘 Vue.js 九个性能优化技巧

    gitHub 源码:https://github.com/Akryum/vue-9-perf-secrets 这篇文章主要参考了 Vue.js 核心成员 Guillaume Chau 在 19 年美国 ...

  8. ELK 性能优化实践

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 一.背景介绍 近一年内对公司的 ELK 日志系统做过性能优 ...

  9. php get 传循环出来的参数_PHP性能优化小技巧

    PHP性能优化小技巧: 1. foreach效率更高,尽量用foreach代替while和for循环. 2. 循环内部不要声明变量,尤其是对象这样的变量. 3. 在多重嵌套循环中,如有可能,应当将最长 ...

最新文章

  1. java按钮位置_java窗口按钮位置设置
  2. 缺少com.umeng.analytics.MobclickAgent包,引入需注意
  3. 产品经验谈:阿里B2B电商-新零售产地供应链的思考与实践
  4. SAP系统上线后的变化
  5. Blockchain Patent Players and domain
  6. ACM MM18 | 用于跨模态检索的综合距离保持自编码器
  7. linux打开服务iis,如何在Linux中引导时列出启动服务?
  8. [HttpClient]HTTPClient PostMethod 中文乱码问题解决方案(2种)
  9. 马斯克再次创造历史!三位太空游客坐五手火箭成功升空,今晚达到空间站,票价3.5亿...
  10. java域名解析到目录_nginx将泛解析的匹配域名绑定到子目录配置方法
  11. 常用的java包_java的常用包
  12. 常用的Java开发工具
  13. 【老生谈算法】matlab实现FFT算法源码——FFT算法
  14. Windows 操作系统成功背后的传奇工程师
  15. Windows安装MySql
  16. 大文件异步分片上传到Seaweed服务器
  17. 电脑锁屏卡死以及任务栏卡死的解决办法
  18. 2021基于Debian的All in One(NAS+软路由)配置教程
  19. SAS实验05 ——方差分析
  20. 数据预测之BP神经网络具体应用以及matlab代码(转)

热门文章

  1. PHP 使用 Ffmpeg 视频截图与视频剪辑
  2. pythoncsv表格对比_对比两个csv文件记录差异
  3. micropython是什么意思_MicroPython能做什么
  4. 使命召唤4战争名言录——战争与和平
  5. 接触软件工程第一周的学习笔记
  6. iframe高度自适应,取消滚动条
  7. 给大家推荐一些免费书籍资源下载
  8. 学霸论文必备的工具,你会用吗?
  9. 2022 主站及创作侧年中总结 - 依然,相信未来、期待未来
  10. 【三维重建】PIFuHD:Multi-Level Pixel-Aligned Implicit Function for High-Resolution 3D Human Digitization