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

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

image.png

作者 | 若冰
来源 | 阿里开发者公众号

You can't manage what you can't measure.
一件事如果你无法衡量它,你就无法管理它。——管理大师 彼得·德鲁克

前言

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

什么是代码覆盖率

1、Dead code

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

image.png

通常我们用 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

image.png

image.png

  • 可度量代码类型:JS CSS
  • 统计可视化形式:

    • 使用率是以byte字节来计算的;
    • 当我们选择一段脚本资源即可在 Source 栏可以看到加载页面时当前资源 run过得代码(蓝色)和没有run过得代码(红色);
  • 缺点:显然,目前大部分网页上的JS脚本基本都是经过混淆压缩打包过后的产物,对于开发者而言,这种覆盖率可读性及参考价值不大。

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

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

image.png

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

image.png

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

image.png

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

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.js
function 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),那么我们 +1
c.s[0]++

function add(a, b) {
  // 如果 add 函数(function)被调用,f +1,且改调用语句 s +1
  c.f[0]++

  c.s[1]++

  return a + b

}
// add 被调出语句 s +1
c.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

image.png

  • HTML

image.png




image.png



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


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

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

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

1、代码分割

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

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

image.png

2、下一步

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

于是有了以下方案:

image.png

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

image.png

运行 Tbox:

image.png

3.插桩构建

  • 依赖 @ali/build-plugin-coverage
  • 通过插桩将源码中插入统计代码
  • 本地构建之后页面全局会注入__coverage__变量(可在页面控制台输出该变量检查插桩是否成功)

image.png

4.分析自动化生成配置

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

image.png

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

image.png

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

image.png

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

写在最后

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


相关文章
|
存储 缓存 监控
如何写出一篇好的技术方案?
近期作者在写某个项目的技术方案时,来来回回修改了许多版,很是苦恼。于是,将自己之前写的和别人写的技术方案都翻出来看了几遍,产生了一些思考,分享给大家。
如何写出一篇好的技术方案?
|
Cloud Native 安全 Java
代码圈复杂度治理小结
我们一直在说系统很复杂,那到底什么是系统复杂度呢?作为团队的稳定性底盘负责人,也经常和大家探讨为什么会因为圈复杂度高而被扣分。那么,怎么才能写的一手可读,可扩展,可维护的好代码?本文作者尝试结合在团队内部的实践,分享下过程中心得。
代码圈复杂度治理小结
|
Java 测试技术 Maven
单元测试运行原理探究
单元测试是软件开发过程中的重要一环,好的单测可以帮助我们更早的发现问题,为系统的稳定运行提供保障。单测还是很好的说明文档,我们往往看单测用例就能够了解到作者对类的设计意图。代码重构时也离不开单测,丰富的单测用例会使我们重构代码时信心满满。
单元测试运行原理探究
|
网络协议 安全 Go
一文详解用eBPF观测HTTP
随着eBPF推出,由于具有高性能、高扩展、安全性等优势,目前已经在网络、安全、可观察等领域广泛应用,同时也诞生了许多优秀的开源项目,如Cilium、Pixie等,而iLogtail 作为阿里内外千万实例可观测数据的采集器,eBPF 网络可观测特性也预计会在未来8月发布。下文主要基于eBPF观测HTTP 1、HTTP 1.1以及HTTP2的角度介绍eBPF的针对可观测场景的应用,同时回顾HTTP 协议自身的发展。
一文详解用eBPF观测HTTP
|
设计模式 IDE Java
谈谈过度设计:因噎废食的陷阱
写软件和造楼房一样需要设计,但是和建筑行业严谨客观的设计规范不同,软件设计常常很主观,且容易引发争论。
3412 4
谈谈过度设计:因噎废食的陷阱
|
敏捷开发 人工智能 开发者
Code Smell 重构你的日常代码-圈复杂度高多层嵌套
圈复杂度(Cyclomatic complexity)[1]是一种代码复杂度的衡量标准,在1976年由Thomas J. McCabe, Sr. 提出。条件分支越多,圈复杂度越高,测试越难覆盖,也越难维护。随着业务的不断演进,代码的不断新增与调整,如果只在原逻辑下加入自己的新逻辑,就会长出一个超高嵌套的“气功波”代码。
1528 7
Code Smell 重构你的日常代码-圈复杂度高多层嵌套
|
存储 机器学习/深度学习 编解码
一文读懂字符编码
说起字符编码,让我想起了科幻巨作《三体-黑暗深林》人类遇到外星文明魔戒的画面
一文读懂字符编码
|
敏捷开发 人工智能 测试技术
如何写出有效的单元测试
一个单元测试是一段自动化的代码,这段代码调用被测试的工作单元,之后对这个单元的单个最终结果的某些假设进行校验。单元测试几乎都是用单元测试框架编写的;只要产品代码不发生变化,单元测试的结果是稳定的。那么如何写出有效的单元测试呢?
如何写出有效的单元测试
|
监控 前端开发 JavaScript
javascript 异常处理的一些经验
为了提升应用稳定性,我们对前端项目开展了脚本异常治理的工作,对生产上报的js error进行了整体排查,试图通过降低脚本异常的发生频次来提升相关告警的准确率,结合最近在这方面阅读的相关资料,尝试阶段性的做个总结,下面我们来介绍下js异常处理的一些经验。
javascript 异常处理的一些经验