├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── README.zh-CN.md ├── bin └── index.js ├── package.json ├── src ├── analyzer │ └── index.js ├── cli │ └── index.js ├── index.js ├── output │ └── index.js ├── performance │ └── index.js └── util │ └── index.js └── test ├── Emitter.test.js └── Util.test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "standard" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | selenium-debug.log 9 | package-lock.json 10 | config.json 11 | exp.json 12 | note 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 liyanfeng(pod4g) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **English** | [中文](./README.zh-CN.md) 2 | 3 |

Hiper

4 | 5 |

🚀 A statistical analysis tool for performance testing

6 | 7 |

8 | 9 | 10 | 11 |

12 | 13 | ## Hiper 14 | 15 | The name is short for **Hi** **per**formance Or **Hi**gh **per**formance 16 | 17 | ## Important 18 | 19 | Hi guys, Please present your issue in English 20 | 21 | 请使用英语提issue 22 | 23 | ## Install 24 | 25 | ``` bash 26 | npm install hiper -g 27 | 28 | # or use yarn: 29 | # yarn global add hiper 30 | ``` 31 | 32 | ## The output 33 | 34 | Notice: `It takes period (m)s to load ...`. the `period` means **This test takes time**. So -n go up and the period go up. not a bug 35 | 36 | ![Hiper](http://7xt9n8.com2.z0.glb.clouddn.com/hiper9.png) 37 | 38 | ## PerformanceTiming 39 | 40 | ![timing](http://7xt9n8.com2.z0.glb.clouddn.com/PerformanceTiming.png) 41 | 42 | | Key | Value | 43 | | :----------------------------- | :------------------------------------------- | 44 | | DNS lookup time | domainLookupEnd - domainLookupStart | 45 | | TCP connect time | connectEnd - connectStart | 46 | | TTFB | responseStart - requestStart | 47 | | Download time of the page | responseEnd - responseStart | 48 | | After DOM Ready download time | domComplete - domInteractive | 49 | | White screen time | domInteractive - navigationStart | 50 | | DOM Ready time | domContentLoadedEventEnd - navigationStart | 51 | | Load time | loadEventEnd - navigationStart | 52 | 53 | https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming 54 | 55 | ## Usage 56 | 57 | ```bash 58 | hiper --help 59 | 60 | Usage: hiper [options] [url] 61 | 62 | 🚀 A statistical analysis tool for performance testing 63 | 64 | Options: 65 | 66 | -v, --version output the version number 67 | -n, --count specified loading times (default: 20) 68 | -c, --config load the configuration file 69 | -u, --useragent to set the useragent 70 | -H, --headless [b] whether to use headless mode (default: true) 71 | -e, --executablePath use the specified chrome browser 72 | --no-cache disable cache (default: false) 73 | --no-javascript disable javascript (default: false) 74 | --no-online disable network (defalut: false) 75 | -h, --help output usage information 76 | ``` 77 | 78 | For instance 79 | 80 | ```bash 81 | # We can omit the protocol header if has omitted, the protocol header will be `https://` 82 | 83 | # The simplest usage 84 | hiper baidu.com 85 | 86 | # if the url has any parameter, surround the url with double quotes 87 | hiper "baidu.com?a=1&b=2" 88 | 89 | # Load the specified page 100 times 90 | hiper -n 100 "baidu.com?a=1&b=2" 91 | 92 | # Load the specified page 100 times without `cache` 93 | hiper -n 100 "baidu.com?a=1&b=2" --no-cache 94 | 95 | # Load the specified page 100 times without `javascript` 96 | hiper -n 100 "baidu.com?a=1&b=2" --no-javascript 97 | 98 | # Load the specified page 100 times with `headless = false` 99 | hiper -n 100 "baidu.com?a=1&b=2" -H false 100 | 101 | # Load the specified page 100 times with set `useragent` 102 | hiper -n 100 "baidu.com?a=1&b=2" -u "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36" 103 | ``` 104 | 105 | ## Config 106 | 107 | #### Support `.json` and `.js` config 108 | 109 | 1. **json** 110 | 111 | ```javascript 112 | { 113 | // options Pointing to a specific chrome executable, this configuration is generally not required unless you want to test a specific version of chrome 114 | "executablePath": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", 115 | // required The url you want to test 116 | "url": "https://example.com", 117 | // options Cookies required for this test. It's usually a cookie for login information Array | Object 118 | "cookies": [{ 119 | "name": "token", 120 | "value": "9+cL224Xh6VuRT", 121 | "domain": "example.com", 122 | "path": "/", 123 | "size": 294, 124 | "httpOnly": true 125 | }], 126 | // options default: 20 Test times 127 | "count": 100, 128 | // options default: true Whether to use headless mode 129 | "headless": true, 130 | // options default: false Disable cache 131 | "noCache": false, 132 | // options default: false Disable javascript 133 | "noJavascript": false, 134 | // options default: false Disable network 135 | "noOnline": false, 136 | // options Set the useragent information 137 | "useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", 138 | // options Set the viewport information 139 | "viewport": { 140 | // options 141 | "width": 375, 142 | // options 143 | "height": 812, 144 | // options default: 1 devicePixelRatio 145 | "deviceScaleFactor": 3, 146 | // options default: false Whether to simulate mobile 147 | "isMobile": false, 148 | // options default: false Whether touch events are supported 149 | "hasTouch": false, 150 | // options default: false Is it horizontal or not 151 | "isLandscape": false 152 | } 153 | } 154 | ``` 155 | 156 | 2. **js** 157 | 158 | Having a JS file for config allows people to use ENV variables. For example, let's say I want to test the site on an authenticated state. I can pass some cookie that is used to identify me through ENV variables and having a JS based config file makes this simple. For example 159 | 160 | ```javascript 161 | 162 | module.exports = { 163 | .... 164 | cookies: [{ 165 | name: 'token', 166 | value: process.env.authtoken, 167 | domain: 'example.com', 168 | path: '/', 169 | httpOnly: true 170 | }], 171 | .... 172 | } 173 | ``` 174 | 175 | ``` bash 176 | # Load the above configuration file (Let's say this file is under /home/) 177 | hiper -c /home/config.json 178 | 179 | # Or you can also use JS files for configuration 180 | hiper -c /home/config.js 181 | ``` 182 | 183 | ## Pain point 184 | 185 | After we have developed a project or optimized the performance of a project, 186 | 187 | how do we measure the performance of this project? 188 | 189 | A common approach is to look at the data in the `performance` and `network` in the `Dev Tool`, record a few key performance metrics, and refresh them a few times before looking at those performance metrics, 190 | 191 | Sometimes we find that due to the small sample size, the current **Network/CPU/Memory** load is heavily impacted, and sometimes the optimized project is slower than before the optimization. 192 | 193 | If there is a tool, request web page many times, and then taking out the various performance indicators averaging, we can **very accurately** know the optimization is positive or negative. 194 | 195 | In addition, you can also make a comparison and get **accurate data** about **how much you have optimized**. This tool is designed to solve the pain point. 196 | 197 | > At the same time, this tool is also a good tool for us to learn about the "browser's process of load and rendering" and "performance optimization", so that we don't get wrong conclusions when there are too few samples 198 | 199 | ## Roadmap 200 | 201 | 1. Better documentation 202 | 2. i18n 203 | 3. Increase the analysis statistics of resource items loaded on the page 204 | 4. Statistical reports can be generated 205 | 5. Data visualization 206 | 207 | ## Contributing 208 | 209 | 1. Fork it 210 | 2. Create your feature branch (git checkout -b my-new-feature) 211 | 3. Commit your changes (git commit -am 'Add some feature') 212 | 4. Push to the branch (git push origin my-new-feature) 213 | 5. Create new Pull Request 214 | 215 | ## License 216 | 217 | [MIT](http://opensource.org/licenses/MIT) 218 | 219 | Welcome Star and PR 220 | 221 | Copyright (c) 2018 liyanfeng(pod4g) 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) | **中文** 2 | 3 |

Hiper

4 | 5 |

🚀 令人愉悦的性能统计分析工具

6 | 7 |

8 | 9 | 10 | 11 |

12 | 13 | ## Hiper 14 | 15 | 可以看成 **Hi** **per**formance的缩写 或者 **Hi**gh **per**formance的缩写 16 | 17 | ## 注意事项 18 | 19 | 请使用英语提issue 20 | 21 | ## 安装 22 | 23 | ``` bash 24 | npm install hiper -g 25 | 26 | # 或者使用 yarn: 27 | # yarn global add hiper 28 | ``` 29 | 30 | ## 输出 31 | 32 | 注意: `It takes period (m)s to load ...`. 这个 `period` 是**运行本次测试所用时间**. 因此,-n 越大,这个数越大 33 | 34 | ![Hiper](http://7xt9n8.com2.z0.glb.clouddn.com/hiper9.png) 35 | 36 | ## 性能指标 37 | 38 | ![timing](http://7xt9n8.com2.z0.glb.clouddn.com/PerformanceTiming.png) 39 | 40 | | Key | Value | 41 | | :----------------------------- | :------------------------------------------- | 42 | | DNS查询耗时 | domainLookupEnd - domainLookupStart | 43 | | TCP连接耗时 | connectEnd - connectStart | 44 | | 第一个Byte到达浏览器的用时 | responseStart - requestStart | 45 | | 页面下载耗时 | responseEnd - responseStart | 46 | | DOM Ready之后又继续下载资源的耗时 | domComplete - domInteractive | 47 | | 白屏时间 | domInteractive - navigationStart | 48 | | DOM Ready 耗时 | domContentLoadedEventEnd - navigationStart | 49 | | 页面加载总耗时 | loadEventEnd - navigationStart | 50 | 51 | https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming 52 | 53 | ## 使用 54 | 55 | ```bash 56 | hiper --help 57 | 58 | Usage: hiper [options] [url] 59 | 60 | 🚀 令人愉悦的性能统计分析工具 61 | 62 | Options: 63 | 64 | -v, --version 输出版本号 65 | -n, --count 指定加载次数(默认20次) 66 | -c, --config 载入指定的配置文件 67 | -u, --useragent 设置useragent 68 | -H, --headless [b] 是否使用无头模式(默认为true) 69 | -e, --executablePath 使用指定的Chrome浏览器 70 | --no-cache 禁用缓存(默认为false) 71 | --no-javascript 禁用JavaScript (默认为false) 72 | --no-online 禁用网络(默认为false) 73 | -h, --help 输出帮助信息 74 | ``` 75 | 76 | 用例 77 | 78 | ```bash 79 | # 当我们省略协议头时,默认会在url前添加`https://` 80 | 81 | # 最简单的用法 82 | hiper baidu.com 83 | 84 | # 如何url中含有任何参数,请使用双引号括起来 85 | hiper "baidu.com?a=1&b=2" 86 | 87 | # 加载指定页面100次 88 | hiper -n 100 "baidu.com?a=1&b=2" 89 | 90 | # 禁用缓存加载指定页面100次 91 | hiper -n 100 "baidu.com?a=1&b=2" --no-cache 92 | 93 | # 禁JavaScript加载指定页面100次 94 | hiper -n 100 "baidu.com?a=1&b=2" --no-javascript 95 | 96 | # 使用GUI形式加载指定页面100次 97 | hiper -n 100 "baidu.com?a=1&b=2" -H false 98 | 99 | # 使用指定useragent加载网页100次 100 | hiper -n 100 "baidu.com?a=1&b=2" -u "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36" 101 | ``` 102 | 103 | ## 配置 104 | 105 | ### 支持 `.json` 和 `.js` 格式的配置文件 106 | 107 | 1. **json** 108 | 109 | ```javascript 110 | { 111 | // options 指向Chrome可执行程序,一般不需要配置此项,除非你想测试某个特定版本的Chrome 112 | "executablePath": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", 113 | // required 要测试的url 114 | "url": "https://example.com", 115 | // options 本次测试需要用到的Cookies,通常是登录信息(即你测试的页面需要登录) Array | Object 116 | "cookies": [{ 117 | "name": "token", 118 | "value": "9+cL224Xh6VuRT", 119 | "domain": "example.com", 120 | "path": "/", 121 | "size": 294, 122 | "httpOnly": true 123 | }], 124 | // options 测试次数 默认为20次 125 | "count": 100, 126 | // options 是否使用无头模式 默认为true 127 | "headless": true, 128 | // options 是否禁用缓存 默认为false 129 | "noCache": false, 130 | // options 是否禁掉JavaScript 默认为false 131 | "noJavascript": false, 132 | // options 是否禁掉网络 默认为false 133 | "noOnline": false, 134 | // options 设置指定的useragent信息 135 | "useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36", 136 | // options 设置视口信息 137 | "viewport": { 138 | // options 139 | "width": 375, 140 | // options 141 | "height": 812, 142 | // options devicePixelRatio 默认为1 143 | "deviceScaleFactor": 3, 144 | // options 是否模拟成mobile 默认为false 145 | "isMobile": false, 146 | // options 是否支持touch时间 默认为false 147 | "hasTouch": false, 148 | // options 是否是横屏模式 默认为false 149 | "isLandscape": false 150 | } 151 | } 152 | ``` 153 | 154 | 2. **js** 155 | 156 | 配置的JS文件允许人们使用ENV变量。例如,假设你想在经过身份验证的状态下测试站点。你可以通过ENV变量传递一些用于标识你的cookie,有一个基于JS的配置文件使这变得很简单。例如 157 | 158 | ```javascript 159 | module.exports = { 160 | .... 161 | cookies: [{ 162 | name: 'token', 163 | value: process.env.authtoken, 164 | domain: 'example.com', 165 | path: '/', 166 | httpOnly: true 167 | }], 168 | .... 169 | } 170 | ``` 171 | 172 | ``` bash 173 | # 载入上述配置文件(假设配置文件在/home/下) 174 | hiper -c /home/config.json 175 | 176 | # 或者你也可以使用js文件作为配置文件 177 | hiper -c /home/config.js 178 | ``` 179 | 180 | ## 痛点 181 | 182 | 我们开发完一个项目或者给一个项目做完性能优化以后,如何来衡量这个项目的性能是否达标? 183 | 184 | 我们的常见方式是在Dev Tool中的performance和network中看数据,记录下几个关键的性能指标,然后刷新几次再看这些性能指标。 185 | 186 | 有时候我们发现,由于样本太少,受当前「网络」、「CPU」、「内存」的繁忙程度的影响很重,有时优化后的项目反而比优化前更慢。 187 | 188 | 如果有一个工具,一次性地请求N次网页,然后把各个性能指标取出来求平均值,我们就能非常准确地知道这个优化是「正优化」还是「负优化」。 189 | 190 | 并且,也可以做对比,拿到「具体优化了多少」的准确数据。这个工具就是为了解决这个痛点的。 191 | 192 | > 同时,这个工具也是学习「浏览器加载渲染网页过程」和「性能优化」的一个利器,因此我们也可以把他作为一个强大的学习辅助工具,不至于让我们在样本过少的情况下得到错误的结论。 193 | 194 | 195 | ## 蓝图 196 | 197 | 1. 更好的文档 198 | 2. 国际化 199 | 3. 页面依赖资源的统计分析 200 | 4. 生成性能统计报告 201 | 5. 数据可视化 202 | 203 | ## 如何贡献 204 | 205 | 1. Fork it 206 | 2. Create your feature branch (git checkout -b my-new-feature) 207 | 3. Commit your changes (git commit -am 'Add some feature') 208 | 4. Push to the branch (git push origin my-new-feature) 209 | 5. Create new Pull Request 210 | 211 | ## 协议 212 | 213 | [MIT](http://opensource.org/licenses/MIT) 214 | 215 | 欢迎Star和PR 216 | 217 | Copyright (c) 2018 liyanfeng(pod4g) 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path') 3 | const fork = require('child_process').fork 4 | const argv = process.argv.slice(2) 5 | 6 | fork(path.resolve(__dirname, '../src/index.js'), argv, { 7 | env: { 8 | NODE_NO_WARNINGS: 1 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": { 3 | "count": 20, 4 | "headless": true, 5 | "noCache": false, 6 | "noJavascript": false, 7 | "noOnline": false, 8 | "noBanner": false 9 | }, 10 | "author": "liyanfeng(pod4g)", 11 | "email": "pod4dop@gmail.com", 12 | "name": "hiper", 13 | "version": "0.0.16", 14 | "description": "🚀 A statistical analysis tool for performance testing", 15 | "keywords": [ 16 | "statistical", 17 | "analysis", 18 | "performance", 19 | "testing", 20 | "tool", 21 | "cli", 22 | "web", 23 | "frontend", 24 | "webpages", 25 | "pages", 26 | "pagespeed", 27 | "websites", 28 | "audits", 29 | "network", 30 | "headless", 31 | "broswer" 32 | ], 33 | "homepage": "https://github.com/pod4g/hiper", 34 | "main": "./src/index.js", 35 | "engines": { 36 | "node": ">=8.0.0", 37 | "npm": ">=5.0.0" 38 | }, 39 | "scripts": { 40 | "test": "jest", 41 | "lint": "eslint benchmarks lib test", 42 | "authors": "git log --format='%aN <%aE>' | sort -u" 43 | }, 44 | "bin": { 45 | "hiper": "./bin/index.js" 46 | }, 47 | "license": "MIT", 48 | "dependencies": { 49 | "chalk": "^2.4.1", 50 | "clear": "^0.1.0", 51 | "clui": "^0.3.6", 52 | "commander": "^2.15.1", 53 | "figlet": "^1.2.0", 54 | "puppeteer": "^1.4.0", 55 | "semver": "^5.5.0" 56 | }, 57 | "devDependencies": { 58 | "eslint": "^4.19.1", 59 | "eslint-config-airbnb-base": "^12.1.0", 60 | "eslint-config-standard": "^11.0.0", 61 | "eslint-plugin-import": "^2.12.0", 62 | "eslint-plugin-node": "^6.0.1", 63 | "eslint-plugin-promise": "^3.8.0", 64 | "eslint-plugin-standard": "^3.1.0", 65 | "jest": "^23.1.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/analyzer/index.js: -------------------------------------------------------------------------------- 1 | const Util = require('../util') 2 | /** 3 | * https://developer.mozilla.org/zh-CN/docs/Web/API/Document/readyState 4 | * https://stackoverflow.com/questions/13346746/document-readystate-on-domcontentloaded 5 | * readyState = interactive,可以近似地作为DOM Ready事件 6 | * readyState = complete,可以近似地作为load事件 7 | * DOM Ready事件在「interactive」和「complete」之间执行 8 | * readyState的值解释: 9 | * loading 文档仍在加载 10 | * interactive 文档已经完成加载,文档已被解析,但是诸如图像,样式表和框架之类的子资源仍在加载 11 | * complete 文档和所有子资源已经全部加载完毕。loads事件即将出发。 12 | */ 13 | class Analyzer { 14 | constructor (data) { 15 | this.data = data 16 | } 17 | /** 18 | * @param {Number} domainLookupStart 返回用户代理对当前文档所属域进行DNS查询开始的时间。 19 | * 如果此请求没有DNS查询过程,如长连接,资源cache,甚至是本地资源等。 那么就返回 fetchStart的值 20 | * @param {Number} domainLookupEnd 返回用户代理对结束对当前文档所属域进行DNS查询的时间。 21 | * 如果此请求没有DNS查询过程,如长连接,资源cache,甚至是本地资源等。那么就返回 fetchStart的值 22 | * @returns {Number} DNS查询耗时 23 | */ 24 | getDNSTime (domainLookupStart, domainLookupEnd) { 25 | return domainLookupEnd - domainLookupStart 26 | } 27 | 28 | /** 29 | * @param {Number} connectStart 返回用户代理向服务器服务器请求文档,开始建立连接的那个时间, 30 | * 如果此连接是一个长连接,又或者直接从缓存中获取资源(即没有与服务器建立连接)。 31 | * 则返回domainLookupEnd的值 32 | * @param {Number} connectEnd 返回用户代理向服务器服务器请求文档, 33 | * 建立连接成功后的那个时间,如果此连接是一个长连接,又或者直接从缓存中获取资源(即没有与服务器建立连接)。 34 | * 则返回domainLookupEnd的值 35 | * @returns {Number} TCP链接耗时 36 | */ 37 | getTCPTime (connectStart, connectEnd) { 38 | return connectEnd - connectStart 39 | } 40 | /** 41 | * @param {Number} responseStart 返回用户代理从服务器、缓存、本地资源中,接收到第一个字节数据的时间 42 | * @param {Number} responseEnd 返回用户代理接收到最后一个字符的时间,和当前连接被关闭的时间中,更早的那个。 43 | * 同样,文档可能来自服务器、缓存、或本地资源 44 | * @returns {Number} 网页本身的下载耗时 45 | */ 46 | getDownloadTime (responseStart, responseEnd) { 47 | return responseEnd - responseStart 48 | } 49 | 50 | /** 51 | * 52 | * @param {Number} domInteractive 准备加载新页面的起始时间 53 | * @param {Number} domComplete readyState = complete的时候 54 | * @returns {Number} 解析DOM Tree耗时 55 | * 这个说法有点儿不严谨,这个只能当做dom加载完毕以后,子资源的下载耗时,名字起的容易让人误解 56 | */ 57 | getAfterDOMReadyTheDownloadTimeOfTheRes (domInteractive, domComplete) { 58 | return domComplete - domInteractive 59 | } 60 | /** 61 | * 62 | * @param {Number} domInteractive 准备加载新页面的起始时间 63 | * @param {Number} responseStart 返回用户代理从服务器、缓存、本地资源中,接收到第一个字节数据的时间 64 | * @returns {Number} 白屏时间 65 | */ 66 | getWhiteScreenTime (navigationStart, domInteractive) { 67 | return domInteractive - navigationStart 68 | } 69 | /** 70 | * 71 | * @param {*} navigationStart 72 | * @param {*} domContentLoadedEventEnd 73 | */ 74 | getDOMReadyTime (navigationStart, domContentLoadedEventEnd) { 75 | return domContentLoadedEventEnd - navigationStart 76 | } 77 | /** 78 | * 79 | * @param {Number} navigationStart 准备加载新页面的起始时间 80 | * @param {Number} loadEventEnd 文档触发load事件结束后的时间。如果load事件没有触发,那么该接口就返回0 81 | * @returns {Number} DOM Ready耗时 82 | */ 83 | getLoadTime (navigationStart, loadEventEnd) { 84 | return loadEventEnd - navigationStart 85 | } 86 | getAverage (total, length) { 87 | return total / length 88 | } 89 | /** 90 | * 91 | * @param {Number} requestStart 92 | * @param {Number} responseStart 93 | * @return {Number} TTFB 94 | * TTFB (Time To First Byte),是最初的网络请求被发起到从服务器接收到第一个字节这段时间, 95 | * 它包含了 TCP连接时间,发送HTTP请求时间和获得响应消息第一个字节的时间。 - 上面是百度百科的解释 96 | * 但是在chrome上,只包含刚开始发送request到接收到第一个byte的时间, 97 | * 发送request前面的预操作则不算在内 98 | */ 99 | getTTFB (requestStart, responseStart) { 100 | return responseStart - requestStart 101 | } 102 | statistics (data = this.data) { 103 | if (!data) { 104 | return 105 | } 106 | 107 | if (!Array.isArray(data)) { 108 | data = [ data ] 109 | } 110 | 111 | let length = data.length // 分析次数 112 | 113 | // DNS查询耗时 114 | let totalDNSTime = 0 115 | // TCP链接耗时 116 | let totalTCPTime = 0 117 | // TTFB 118 | let totalTTFBTime = 0 119 | // donwload资源耗时 120 | let totalDownloadTime = 0 121 | // 解析dom树耗时 122 | let totalAfterDOMReadyTheDownloadTimeOfTheRes = 0 123 | // 白屏时间 124 | let totalWhiteScreenTime = 0 125 | // domready时间 126 | let totalDOMReadyTime = 0 127 | // onload时间 128 | let totalLoadTime = 0 129 | 130 | for (let item of data) { 131 | let { total } = JSON.parse(item) 132 | // console.log(entries) 133 | let { 134 | navigationStart, 135 | domainLookupStart, 136 | domainLookupEnd, 137 | connectStart, 138 | connectEnd, 139 | requestStart, 140 | responseStart, 141 | responseEnd, 142 | // domLoading, 143 | domInteractive, 144 | // domContentLoadedEventStart, 145 | domContentLoadedEventEnd, 146 | domComplete, 147 | // loadEventStart, 148 | loadEventEnd 149 | } = total.timing 150 | 151 | totalDNSTime += this.getDNSTime(domainLookupStart, domainLookupEnd) 152 | totalTCPTime += this.getTCPTime(connectStart, connectEnd) 153 | totalTTFBTime += this.getTTFB(requestStart, responseStart) 154 | totalDownloadTime += this.getDownloadTime(responseStart, responseEnd) 155 | totalAfterDOMReadyTheDownloadTimeOfTheRes += this.getAfterDOMReadyTheDownloadTimeOfTheRes(domInteractive, domComplete) 156 | totalWhiteScreenTime += this.getWhiteScreenTime(navigationStart, domInteractive) 157 | totalDOMReadyTime += this.getDOMReadyTime(navigationStart, domContentLoadedEventEnd) 158 | totalLoadTime += this.getLoadTime(navigationStart, loadEventEnd) 159 | } 160 | 161 | // console.log('DNS lookup time:', Util.formatMSToHumanReadable(this.getAverage(totalDNSTime, length))) 162 | // console.log('TCP connect time:', Util.formatMSToHumanReadable(this.getAverage(totalTCPTime, length))) 163 | // console.log('TTFB:', Util.formatMSToHumanReadable(this.getAverage(totalTTFBTime, length))) 164 | // console.log('Download time of the page:', Util.formatMSToHumanReadable(this.getAverage(totalDownloadTime, length))) 165 | // console.log('After DOM Ready the download time of resources:', Util.formatMSToHumanReadable(this.getAverage(totalAfterDOMReadyTheDownloadTimeOfTheRes, length))) 166 | // console.log('White screen time:', Util.formatMSToHumanReadable(this.getAverage(totalWhiteScreenTime, length))) 167 | // console.log('DOM Ready time:', Util.formatMSToHumanReadable(this.getAverage(totalDOMReadyTime, length))) 168 | // console.log('Load time:', Util.formatMSToHumanReadable(this.getAverage(totalLoadTime, length))) 169 | // console.log('DNS查询耗时:', Util.formatMSToHumanReadable(this.getAverage(totalDNSTime, length))) 170 | // console.log('TCP连接耗时:', Util.formatMSToHumanReadable(this.getAverage(totalTCPTime, length))) 171 | // console.log('TTFB:', Util.formatMSToHumanReadable(this.getAverage(totalTTFBTime, length))) 172 | // console.log('页面下载耗时:', Util.formatMSToHumanReadable(this.getAverage(totalDownloadTime, length))) 173 | // console.log('白屏时间:', Util.formatMSToHumanReadable(this.getAverage(totalWhiteScreenTime, length))) 174 | // console.log('DOM Ready耗时:', Util.formatMSToHumanReadable(this.getAverage(totalDOMReadyTime, length))) 175 | // console.log('DOM Ready之后继续进行资源下载的耗时:', Util.formatMSToHumanReadable(this.getAverage(totalAfterDOMReadyTheDownloadTimeOfTheRes, length))) 176 | // console.log('Load时间:', Util.formatMSToHumanReadable(this.getAverage(totalLoadTime, length))) 177 | // console.log(`\n`) 178 | 179 | return { 180 | total: { 181 | dnsTime: Util.formatMSToHumanReadable(this.getAverage(totalDNSTime, length)), 182 | tcpTime: Util.formatMSToHumanReadable(this.getAverage(totalTCPTime, length)), 183 | TTFB: Util.formatMSToHumanReadable(this.getAverage(totalTTFBTime, length)), 184 | pageDownloadTime: Util.formatMSToHumanReadable(this.getAverage(totalDownloadTime, length)), 185 | whiteScreenTime: Util.formatMSToHumanReadable(this.getAverage(totalWhiteScreenTime, length)), 186 | DOMReadyTime: Util.formatMSToHumanReadable(this.getAverage(totalDOMReadyTime, length)), 187 | afterDOMReadyDownloadTime: Util.formatMSToHumanReadable(this.getAverage(totalAfterDOMReadyTheDownloadTimeOfTheRes, length)), 188 | loadTime: Util.formatMSToHumanReadable(this.getAverage(totalLoadTime, length)) 189 | } 190 | } 191 | } 192 | } 193 | 194 | module.exports = Analyzer 195 | -------------------------------------------------------------------------------- /src/cli/index.js: -------------------------------------------------------------------------------- 1 | const program = require('commander') 2 | const pjson = require('../../package.json') 3 | const path = require('path') 4 | const Util = require('../util') 5 | 6 | const { 7 | _args, 8 | version, 9 | description 10 | } = pjson 11 | 12 | module.exports = class Cli { 13 | parseJSONFile (filePath) { 14 | filePath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath) 15 | let data = null 16 | try { 17 | data = require(filePath) 18 | if (data) { 19 | let { noBanner, noCache, noJavascript, noOnline } = data 20 | data.banner = !noBanner 21 | data.cache = !noCache 22 | data.javascript = !noJavascript 23 | data.online = !noOnline 24 | delete data.noBanner 25 | delete data.noCache 26 | delete data.noJavascript 27 | delete data.noOnline 28 | } 29 | } catch (error) { 30 | console.log(error) 31 | } 32 | return data 33 | } 34 | 35 | headless (b) { 36 | if (b === 'true') b = true 37 | if (b === 'false') b = false 38 | return b 39 | } 40 | 41 | monitor () { 42 | let url = null 43 | program 44 | .version(version, '-v, --version') 45 | .usage('[options] [url]') 46 | .description(description) 47 | .arguments('') 48 | .action(u => url = u) // eslint-disable-line 49 | .option('-n, --count ', 'specified loading times (default: 20)', parseInt) 50 | .option('-c, --config ', 'load the configuration file', this.parseJSONFile) 51 | .option('-u, --useragent ', 'to set the useragent') 52 | .option('-H, --headless [b]', 'whether to use headless mode (default: true)', this.headless) 53 | .option('-e, --executablePath ', 'use the specified chrome browser') 54 | .option('--no-banner', 'disable banner (default: false)') 55 | .option('--no-cache', 'disable cache (default: false)') 56 | .option('--no-javascript', 'disable javascript (default: false)') 57 | .option('--no-online', 'disable network (defalut: false)') 58 | .parse(process.argv) 59 | 60 | let { 61 | executablePath, 62 | count, 63 | config, 64 | headless, 65 | useragent, 66 | banner, 67 | cache, 68 | javascript, 69 | online 70 | } = program 71 | 72 | if (!config) config = {} 73 | 74 | url = Util.urlNormalize(url || config.url) 75 | // 给cli参数赋予默认值 76 | if (!count) { 77 | count = config.count || _args.count 78 | } 79 | 80 | if (useragent == null) { 81 | useragent = config.useragent 82 | } 83 | 84 | if (headless == null) { 85 | headless = config.headless || _args.headless 86 | } 87 | 88 | if (banner == null) { 89 | banner = config.banner || !_args.banner 90 | } 91 | 92 | if (cache == null) { 93 | cache = config.cache || !_args.noCache 94 | } 95 | 96 | if (javascript == null) { 97 | javascript = config.javascript || !_args.noJavascript 98 | } 99 | 100 | if (online == null) { 101 | online = config.online || !_args.noOnline 102 | } 103 | 104 | if (executablePath == null) { 105 | executablePath = config.executablePath 106 | } 107 | 108 | if (config.viewport) { 109 | config.viewport.deviceScaleFactor = config.viewport.deviceScaleFactor || 1 110 | config.viewport.isMobile = config.viewport.isMobile || false 111 | config.viewport.hasTouch = config.viewport.hasTouch || false 112 | config.viewport.isLandscape = config.viewport.isLandscape || false 113 | } 114 | 115 | if (config.cookies && !Array.isArray(config.cookies)) { 116 | config.cookies = [config.cookies] 117 | } 118 | 119 | let opts = Object.assign(config, { 120 | executablePath, 121 | url, 122 | count, 123 | headless, 124 | useragent, 125 | banner, 126 | cache, 127 | javascript, 128 | online 129 | }) 130 | 131 | global.__hiper__ = opts 132 | 133 | return opts 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver') 2 | const chalk = require('chalk') 3 | const requiredNodeVersion = require('../package.json').engines.node 4 | 5 | if (!semver.satisfies(process.version, requiredNodeVersion)) { 6 | console.log(chalk.red( 7 | `\n[Hiper] Minimum Node version not met:` + 8 | `\nYou are using Node ${process.version}, but Hiper ` + 9 | `requries Node ${requiredNodeVersion}.\nPlease upgrade your Node version.\n` 10 | )) 11 | process.exit(1) 12 | } 13 | 14 | // 接受cli参数 15 | // 装配opts 16 | // 调用broswer拿到数据 17 | // 调用分析模块 18 | // 调用output 19 | /** 20 | * Module dependencies. 21 | */ 22 | // 命令行对象 23 | const Cli = require('../src/cli') 24 | const Outputer = require('../src/output') 25 | // 性能数据生成对象 26 | const Performance = require('../src/performance') 27 | // 统计分析对象 28 | const Analyzer = require('../src/analyzer') 29 | const cli = new Cli() 30 | const performance = new Performance() 31 | const analyzer = new Analyzer() 32 | const outputer = new Outputer() 33 | 34 | // 监听命令行 35 | let opts = cli.monitor() 36 | performance.run(opts).then(async statisticData => { 37 | let data = await analyzer.statistics(statisticData) 38 | // console.log('data:', data) 39 | outputer.output(data) 40 | }) 41 | 42 | // console.log(JSON.stringify(opts)) 43 | -------------------------------------------------------------------------------- /src/output/index.js: -------------------------------------------------------------------------------- 1 | const figlet = require('figlet') 2 | const clui = require('clui') 3 | const clc = require('cli-color') 4 | const Line = clui.Line 5 | 6 | module.exports = class Outputer { 7 | output (data) { 8 | if (!data) return 9 | let { total } = data 10 | let { 11 | dnsTime, 12 | tcpTime, 13 | TTFB, 14 | pageDownloadTime, 15 | whiteScreenTime, 16 | DOMReadyTime, 17 | afterDOMReadyDownloadTime, 18 | loadTime 19 | } = total 20 | if (global.__hiper__.banner) { 21 | console.log('\n') 22 | console.log(figlet.textSync('Hiper')) 23 | console.log('\n') 24 | // console.log(`🚀 加载 ${global.__hiper__.url} ${global.__hiper__.count} 次 用时 ${(global.__hiper__.runInterval) / 1000} s`) 25 | console.log(`🚀 It takes ${(global.__hiper__.runInterval) / 1000} s to load \`${global.__hiper__.url}\` ${global.__hiper__.count} times`) 26 | console.log('\n') 27 | } 28 | new Line() 29 | .padding(2) 30 | .column('Run interval', 32) 31 | .column(`${(global.__hiper__.runInterval) / 1000} s`, 20, [clc.cyan]) 32 | .fill() 33 | .output() 34 | new Line() 35 | .padding(2) 36 | .column('Total load times', 32) 37 | .column(global.__hiper__.count.toString(), 20, [clc.cyan]) 38 | .fill() 39 | .output() 40 | new Line() 41 | .padding(2) 42 | .column('DNS lookup time', 32) 43 | .column(dnsTime, 20, [clc.cyan]) 44 | .fill() 45 | .output() 46 | new Line() 47 | .padding(2) 48 | .column('TCP connect time', 32) 49 | .column(tcpTime, 20, [clc.cyan]) 50 | .fill() 51 | .output() 52 | new Line() 53 | .padding(2) 54 | .column('TTFB', 32) 55 | .column(TTFB, 20, [clc.cyan]) 56 | .fill() 57 | .output() 58 | new Line() 59 | .padding(2) 60 | .column('Download time of the page', 32) 61 | .column(pageDownloadTime, 20, [clc.cyan]) 62 | .fill() 63 | .output() 64 | new Line() 65 | .padding(2) 66 | .column('After DOM Ready download time', 32) 67 | .column(afterDOMReadyDownloadTime, 20, [clc.cyan]) 68 | .fill() 69 | .output() 70 | new Line() 71 | .padding(2) 72 | .column('White screen time', 32) 73 | .column(whiteScreenTime, 20, [clc.cyan]) 74 | .fill() 75 | .output() 76 | new Line() 77 | .padding(2) 78 | .column('DOM Ready time', 32) 79 | .column(DOMReadyTime, 20, [clc.cyan]) 80 | .fill() 81 | .output() 82 | new Line() 83 | .padding(2) 84 | .column('Load time', 32) 85 | .column(loadTime, 20, [clc.cyan]) 86 | .fill() 87 | .output() 88 | if (global.__hiper__.banner) { 89 | console.log('\n') 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/performance/index.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer') 2 | 3 | module.exports = class Performance { 4 | constructor (opts) { 5 | this.opts = opts 6 | } 7 | 8 | async run (opts = this.opts) { 9 | let startTimestamp = Date.now() 10 | let { 11 | executablePath, 12 | url, 13 | count, 14 | headless, 15 | useragent, 16 | viewport, 17 | cookies, 18 | cache, 19 | javascript, 20 | online 21 | } = opts 22 | 23 | let launchOpts = { 24 | headless 25 | // args: ['--unlimited-storage', '--full-memory-crash-report'] 26 | } 27 | 28 | if (executablePath) { 29 | launchOpts.executablePath = executablePath 30 | } 31 | 32 | const browser = await puppeteer.launch(launchOpts) 33 | let tab = await browser.newPage() 34 | let loadTasks = [] 35 | let loadEvents = [] 36 | let settingTasks = [ 37 | tab.setCacheEnabled(cache), 38 | tab.setJavaScriptEnabled(javascript), 39 | tab.setOfflineMode(!online), 40 | tab.setRequestInterception(false) 41 | ] 42 | if (cookies) { 43 | settingTasks.push(tab.setCookie(...cookies)) 44 | } 45 | if (viewport) { 46 | settingTasks.push(tab.setViewport(viewport)) 47 | } 48 | if (useragent) { 49 | settingTasks.push(tab.setUserAgent(useragent)) 50 | } 51 | await Promise.all(settingTasks) 52 | for (let i = 0; i < count; i++) { 53 | loadTasks.push( 54 | tab.goto(url, { timeout: 172800000, waitUntil: 'load' }) 55 | ) 56 | let loadHandler = () => { 57 | loadEvents.push(tab.evaluate(() => { 58 | let total = window.performance 59 | let entries = total.getEntries() 60 | return JSON.stringify({ total, entries }) 61 | })) 62 | tab.removeListener('load', loadHandler) 63 | } 64 | tab.on('load', loadHandler) 65 | } 66 | await Promise.all(loadTasks) 67 | let performances = await Promise.all(loadEvents) 68 | setTimeout(() => browser.close()) 69 | global.__hiper__.runInterval = Date.now() - startTimestamp 70 | // console.log(`跑完 ${global.__hiper__.url} 全部性能测试用时:${(Date.now() - startTimestamp) / 1000}s`) 71 | // console.log(`\n---------------------- 🚀 各项指标平均耗时(${global.__hiper__.count}次)----------------------\n`) 72 | return performances 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | module.exports = class Util { 2 | /** 3 | * @param {Number} ms 4 | * 把毫秒数转化为人类可读的字符串 5 | */ 6 | static formatMSToHumanReadable (ms, readable = true) { 7 | let ret = `${(ms).toFixed(2)} ms` 8 | if (!readable) return ret 9 | const ONE_SECOND = 1000 10 | const ONE_MINUTE = 60 * ONE_SECOND 11 | const ONE_HORE = 60 * ONE_MINUTE 12 | // 小于1秒,那么用毫秒为单位 13 | if (ms >= ONE_SECOND && ms < ONE_MINUTE) { 14 | // 大于一秒小于一分钟,用秒作为单位 15 | ret = `${(ms / 1000).toFixed(2)} s` 16 | } else if (ms >= ONE_MINUTE && ms < ONE_HORE) { 17 | // 大于一分钟,小于一小时,用分钟作单位 18 | ret = `${(ms / 1000 / 60).toFixed(2)} m` 19 | } else if (ms >= ONE_HORE) { 20 | // 大于一个小时,用小时作单位 21 | ret = `${(ms / 1000 / 60 / 60).toFixed(2)} h` 22 | } 23 | return ret 24 | } 25 | 26 | static urlNormalize (url) { 27 | if (!url) return '' 28 | if (url.startsWith('//')) { 29 | return `https:${url}` 30 | } 31 | if (!/^https?:\/\//.test(url)) { 32 | return `https://${url}` 33 | } 34 | return url 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Emitter.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const EventEmitter = require('events').EventEmitter 3 | const emitter = new EventEmitter() 4 | emitter.setMaxListeners(200) 5 | test('EventEmitter', () => { 6 | let count = 0 7 | let i = 0 8 | for (i = 0; i < 300; i++) { 9 | emitter.on('test', () => { 10 | count++ 11 | }) 12 | } 13 | emitter.emit('test') 14 | console.log('执行次数:', count) 15 | expect(count).toBe(i) 16 | }) 17 | -------------------------------------------------------------------------------- /test/Util.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const Util = require('../src/util') 3 | test('Util.formatMSToHumanReadable', () => { 4 | expect(Util.formatMSToHumanReadable(331)).toBe('331.00 ms') 5 | expect(Util.formatMSToHumanReadable(1000)).toBe('1.00 s') 6 | expect(Util.formatMSToHumanReadable(2112)).toBe('2.11 s') 7 | expect(Util.formatMSToHumanReadable(2116)).toBe('2.12 s') 8 | expect(Util.formatMSToHumanReadable(3600000)).toBe('1.00 h') 9 | expect(Util.formatMSToHumanReadable(3600011)).toBe('1.00 h') 10 | expect(Util.formatMSToHumanReadable(7200000)).toBe('2.00 h') 11 | expect(Util.formatMSToHumanReadable(7200000)).toBe('2.00 h') 12 | }) 13 | 14 | test('Util.urlNormalize', () => { 15 | expect(Util.urlNormalize(null)).toBe('') 16 | expect(Util.urlNormalize(void 0)).toBe('') 17 | expect(Util.urlNormalize('')).toBe('') 18 | expect(Util.urlNormalize('http://www.diaox2.com')).toBe('http://www.diaox2.com') 19 | expect(Util.urlNormalize('https://www.diaox2.com')).toBe('https://www.diaox2.com') 20 | expect(Util.urlNormalize('www.diaox2.com')).toBe('http://www.diaox2.com') 21 | expect(Util.urlNormalize('//www.diaox2.com')).toBe('http://www.diaox2.com') 22 | }) 23 | --------------------------------------------------------------------------------