Redian新闻
>
代码覆盖率在性能优化上的一种可行应用

代码覆盖率在性能优化上的一种可行应用

科技


You can't manage what you can't measure.

一件事如果你无法衡量它,你就无法管理它。——管理大师 彼得·德鲁克


前     言


JavaScript 是前端应用主要语言,相较于其他平台编程语言,JS资源多数情况下要通过网络进行加载,那么代码的体积直接影响了页面加载执行时间。“无效的代码”的多寡直接影响到了我们的代码质量,所以度量代码的执行覆盖率是一项重要的优化前置工作。

什么是代码覆盖率


1、Dead code

Dead code 也叫无用代码,这个概念应是在编译时静态分析出的对执行无影响的代码,举个例子:

// a.jsconst a = 1;const b = 2/* dead code */export default a;// index.jsimport a from './a.js';export default function({  console.log(a);}

通常我们用 Tree Shaking 在编译时移除这些 dead code以减小代码体积。


2、冗余代码


而代码覆盖率里所提到的冗余代码 和 Dead Code 又略有不同,简单来说Dead code适用于编译时,而 Code coverage 适用于运行时。

Dead code 是任何情况下都不会执行的代码,所以可以再编译阶段将其剔除。

冗余代码  是某些特定的业务逻辑之下并不会执行到这些代码逻辑(比如:在首屏加载时,某个前端组件完全不会加载,那么对于“首屏”这个业务逻辑用例来讲,该前端代码就是冗余的)


3、代码覆盖率


代码覆盖率(Code coverage)是软件测试中的一种度量指标。即描述测试过程中(运行时)被执行的源代码占全部源代码的比例。

怎么度量代码覆盖率


1、Chrome 浏览器 Dev Tools


chrome 浏览器的 DevTools 给我们提供了度量页面代码(JS、CSS)覆盖率的工具 Coverage。

  • 使用方式:Dev tools —— More tools —— Coverage









  • 可度量代码类型:JS CSS


  • 统计可视化形式:


  • 使用率是以byte字节来计算的;


  • 当我们选择一段脚本资源即可在 Source 栏可以看到加载页面时当前资源 run过得代码(蓝色)和没有run过得代码(红色);


  • 缺点:显然,目前大部分网页上的JS脚本基本都是经过混淆压缩打包过后的产物,对于开发者而言,这种覆盖率可读性及参考价值不大。



TIPS:当然,如果在拥有 source map 的情况下也是可以用浏览器查看源代码的覆盖率的:


1.在 source tab 中找到当前页面的 js 资源文件(当然已经被混淆的面目全非)





2.输入 sourcemap URL(以 def 发布平台为例,在构建结果中可找到)





3. 在 webpack:// 目录下即可查看对应源码的大致覆盖率(不过没有什么消费价值)




那么问题来了,有没有一种方法可以令开发者了解 源代码 的代码覆盖率的值呢?



2、Istanbul(NYC)


这个软件以土耳其最大城市伊斯坦布尔命名,因为土耳其地毯世界闻名,而地毯则是用来覆盖的。



Istanbul或者 NYC(New York City,基于 istanbul 实现) 是度量 JavaScript 程序的代码覆盖率工具,目前绝大多数的node代码测试框架使用该工具来获得测试报告,其有四个测量维度:



line coverage(行覆盖率-每一行是否都执行了) 【一般我们关注这个信息】   function coverage(函数覆盖率-每个函数是否都调用了)   branch coverage(分支覆盖率-是否每个 if 代码块都执行了)   statement coverage(语句覆盖率-是否每个语句都执行了)


  • 可以度量的代码类型:JS TS


  • 统计可视化的形式:


  • HTML

  • terminal


  • 缺点:目前使用 istanbul 度量网页前端JS代码覆盖率没有非侵入的方案,采用的是在编译构建时修改构建结果的方式埋入统计代码,再在运行时进行统计展示。



我们可以使用 babel-plugin-istanbul 插件在对源代码在 AST 级别进行包装重写,这种编译方式也叫 代码插桩 / 插桩构建(instrument)



3、插桩构建


我们如果要度量这一段代码哪些代码执行了 哪些代码没有执行,我们会怎么做呢?


// add.jsfunction add(a, b{  return a + b}module.exports = { add }



我们可以很容易的想到加一些“装饰性”的代码在我们的源码里面,那么当代码一行一行的执行到某处时,那么我们就在全局环境变量中记录一下:



// 全局对象记录了 __coverage__ 记录了上面代码中的语句和函数的执行次数const c = (window.__coverage__ = {  // "f" 表示每一个 function 被执行的次数  // 当前代码只有一个 function 因此,f 数组只有一个 且记录值为 0  f: [0],  // "s" 表示每一个 statement 被执行的次数  // 3 个 statement 全部都以 0 赋值  s: [0, 0, 0],
})
// 函数定义是一个语句(statement),那么我们 +1c.s[0]++
function add(a, b) { // 如果 add 函数(function)被调用,f +1,且改调用语句 s +1 c.f[0]++
c.s[1]++
return a + b
}// add 被调出语句 s +1c.s[2]++module.exports = { add }



istabul 确实也是这么做的,babel-plugin-istanbul 在构建过程中分析 AST 并将相应统计单元(语句、函数、分支等)做装饰代码的添加,最终在代码运行之后,输出一份 json 格式的数据:



{
    "/Users/bairuobing/test/istanbul.js":{        "path":"/Users/bairuobing/test/istanbul.js",        "s":{            "1":1,            "2":0,            "3":1        },        "b":{
        },        "f":{            "1":0 },
        "fnMap":{ // function 的开始结束位置信息            "1":{                "name":"add",                "line":1,                "loc":{                    "start":{                        "line":1,
                        "column":0                    },                    "end":{                        "line":1,                        "column":19                    }                }            }        },        "statementMap":{ // statement 的开始结束位置信息            "1":{                "start":{                    "line":1,                    "column":0                },                "end":{                    "line":3,                    "column":1                }            },            "2":{                "start":{                    "line":2,                    "column":4                },                "end":{                    "line":2,                    "column":16                }            },            "3":{                "start":{                    "line":4,                    "column":0                },                "end":{                    "line":4,                    "column":24                }            }        },        "branchMap":{ // branch 的开始结束位置信息
        }    }}



当我们在运行代码过后,得到了上面的 json 便可以消费它了。



# terminal 形式输出nyc report --reporter=text# HTML 形式输出nyc report --reporter=lcov --exclude-after-remap=false



  • terminal






  • HTML









代码覆盖率在 iHome Rax开发套件 Tbox 中的应用




tips:tbox 每平每屋 消费者端 本地开发套件



既然我们知道了源代码的代码覆盖率,我们可以用它为性能优化做些什么贡献呢?



当工程主 bundle 较大,那么采用拆包较大的/无用的前端组件来瘦身首屏主 JS 包不失为一种可行的选择,此时就可以根据代码覆盖率来决定优化哪些代码。



1、代码分割


React.lazy 已经为我们提供了一种不错的思路,就是利用动态加载模块规范 import() (webpack对import()解析为代码分割)的能力来实现前端组件代码懒加载/动态加载。



以此为灵感,那么为何不将某些组件通过动态引入的方式加载,来以此换取首页 bundle 的瘦身呢?



// 动态引入组件// ThisIsBigModimport { createElement, useState, useEffect } from 'rax';export default (props) => {  const [AsyncMod, setAsyncMod] = useState(null);  useEffect(() => {    const load = async () => {      const Module = await import('./ThisIsBigMod'); // 关键      try {        setAsyncMod(Module);      } catch (e) {        console.log(e);      }    };    load();  }, []);
  if (!AsyncMod || !AsyncMod.default) {    return null;  }    return <AsyncMod.default {...props} />;};



2、下一步


我们能通过代码覆盖率统计出哪些组件的代码首屏使用率为0(或者门槛值30%以下),并在项目工程中自动生成一个持久化的文件配置(app.json中),之后依据配置将这些低使用率的组件代码在生产构建时将产物代码改写为动态引入。



于是有了以下方案:







3、如何使用


1.该功能需要项目下安装以下 build 插件(如 tbox 新建的项目已安装以下插件可忽略):


  • @ali/build-plugin-coverage

  • @ali/build-plugin-async-components



tnpm install --save-dev @ali/build-plugin-coverage  @ali/build-plugin-async-components



2. build.json


// build.json"plugins": [  ......  "@ali/build-plugin-coverage",  [    "@ali/build-plugin-async-components",    {      "active"true    }  ]]


运行 Tbox:




3. 插桩构建


  • 依赖 @ali/build-plugin-coverage


  • 通过插桩将源码中插入统计代码


  • 本地构建之后页面全局会注入__coverage__变量(可在页面控制台输出该变量检查插桩是否成功)





4. 分析自动化生成配置


  • 等待完成首屏渲染(或者完成自定义的一系列行为用例),此刻插桩代码已经完成了代码使用率的统计







  • 打开 Tlog 小工具 点击代码优化->生成源代码优化配置,此刻 Tbox 本地服务已经接收到了发来的__coverage__并完成后续的代码覆盖率分析,通过分析使用率低于门槛值的组件文件,将这些组件的项目相对路径写入 app.json 的 modsPath 字段下


  • 此刻 @ali/build-plugin-async-components 会根据 modsPath 配置自动将组件构建为动态引入的方式






  • 如果您想通过自己的配置来完成组件异步化,请直接手动修改 app.json 里的 modsPath 字段,只需依赖 @ali/build-plugin-async-components 插件再次构件即可




  • 此时我们条件加载被异步化的组件会发现,BigMod 组件已经被动态的拆包引入了,页面主 js 包也得到了瘦身,搞定!


写在最后



istanbul 在 node 环境下跑测试用例代码能度量覆盖率是由于其对运行时模块加载器的源代码拦截,但是比较遗憾的是,本文介绍的代码插桩分析覆盖率这会引入一些多余的桩代码,或许采用 puppeteer 无头浏览器提供的Coverage api + sourceMap 逆编译的思路来进行度量是一种更加完美的方式,期待与诸君一起探索,继续努力!




《微服务治理技术白皮书》免费下载


该白皮书历经半年多筹备,长达379页。希望通过本书,能对高效解决云原生架构下的微服务治理难题,起到一点作用!


点击阅读原文查看详情!


微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
人得的病只有两种,一种是不必治的,一种是治不好的无暇顾及WXC大事 完成小事要紧足球小作文: 唯一一种可被允许的“矫情”过去5年,PolarDB云原生数据库是如何进行性能优化的?现在性能拉满的手机芯片,原来是被它牵着鼻子走的?与时聚汇 | 另一种可能性:今日中国青年生活观专家紧急提醒:这种可怕的灾难,可能要在澳卷土重来了!维州也遭殃加州『FIELD OF LIGHT』五万颗灯泡覆盖在山坡上的浪漫奇景【微报告】低代码/零代码平台应用实践与趋势研究:制造业篇 | 甲子光年咱长那肺结节有多少种可能?只是个炎症的可能性到底多大?高盛观点 | 半导体供应链本地化可行吗?拜登喊话加油站降价遭贝索斯怒怼,通胀重压下拜登支持率在低位徘徊年挣五万也能过日子龙卷风健康快递 153又一个她,倒在性骚扰的拳头下还能这么赖账?揭秘一种可转债的创新玩法澳洲冬季感冒病毒横行,这7种可帮助恢复的食物你一定要尝试!国际 | 金融科技能否通过开放式银行API建立可行业务?从钟薛高到茅台,国产冰淇淋IP化可行吗Flink & 低代码:为应用实时计算铺平道路延缓左室传导疾病发生,强化血压控制或许可行!前端性能优化实战一日一诗:“飘,仿佛一种叶子/ 飘的曲线,舒展或打着褶皱/ 反射一种光芒”||周启垠:飘(读诗版)架构即代码:编码下一代企业(应用)架构体系旧金山新冠病毒阳性率在加州最高白瘦幼在性感妈生脸面前不值一提!她换头换的太值了…性能也是一种奢侈:加拿大鹅销售创纪录 继续看好中国市场结婚自愈手册S2—22|找一个肤浅的丈夫到底可行吗?你是悲观派还是乐观派?科学家预测:未来五年全球人类生活的三种可能性代价微笑提案,创新的另一种可能悲从中来,洒泪遥送我亲爱的妈妈总是对医院的诊断结果存疑,是一种心理疾病吗?——知乎上的一个好问题微商内容打法在小红书异军突起,小投入高回报模式是否可行?请问未打疫苗,通过医生开具医疗豁免证明,然后入境澳洲是否可行?
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。