乐趣区

关于前端:代码覆盖率在性能优化上的一种可行应用

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

You can’t manage what you can’t measure.
一件事如果你无奈掂量它,你就无奈治理它。——治理巨匠 彼得·德鲁克

前言

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

什么是代码覆盖率

1、Dead code

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

// a.js
const a = 1;
const b = 2; /* dead code */
export default a;
// index.js
import 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 的状况下也是能够用浏览器查看源代码的覆盖率的:

在 source tab 中找到以后页面的 js 资源文件(当然曾经被混同的面目全非)

输出 sourcemap URL(以 def 公布平台为例,在构建后果中可找到)

在 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.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

HTML

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

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

既然咱们晓得了源代码的代码覆盖率,咱们能够用它为性能优化做些什么奉献呢?

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

1、代码宰割

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

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

// 动静引入组件
// ThisIsBigMod
import {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、如何应用

  • 该性能须要我的项目下装置以下 build 插件:

@ali/build-plugin-coverage
@ali/build-plugin-async-components

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

build.json

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

运行 Tbox:

插桩构建

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

剖析自动化生成配置

期待实现首屏渲染(或者实现自定义的一系列行为用例),此刻插桩代码曾经实现了代码使用率的统计



关上 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 逆编译的思路来进行度量是一种更加完满的形式,期待与诸君一起摸索,持续致力!

原文链接
本文为阿里云原创内容,未经容许不得转载。

退出移动版