├── .github └── workflows │ ├── codeql.yml │ └── linter.yml ├── .gitignore ├── LICENSE ├── README.md ├── eslint.config.js ├── index.js ├── lib ├── css.js ├── filter.js ├── html.js ├── index.js ├── js.js └── util.js ├── package.json ├── renovate.json └── test ├── deploy.sh └── index.js /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "58 14 * * 4" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Use Node.js 13 | uses: actions/setup-node@v4 14 | - run: npm install 15 | - run: npm run lint 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | .DS_Store 4 | tmp/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 NexT [Reloaded] 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hexo-optimize 2 | 3 | [![Build Status][github-image]][github-url] 4 | [![npm-image]][npm-url] 5 | [![lic-image]](LICENSE) 6 | 7 | A hexo plugin that optimize the pages loading speed. It will automatically filter your blog files and make optimizations. For example: 8 | 9 | - Minify CSS, JS and HTML files 10 | - Inline specific CSS files (like `main.css`) directly into the html page to improve First Contentful Paint performance 11 | - Add content hash to static resource filenames (versioning) for better cache control 12 | 13 | It will help you get a higher score in the [Google PageSpeed Insights](https://pagespeed.web.dev). 14 | 15 | ## Installation 16 | 17 | ![size-image] 18 | [![dm-image]][npm-url] 19 | [![dt-image]][npm-url] 20 | 21 | ```bash 22 | npm install hexo-optimize 23 | ``` 24 | 25 | ## Usage 26 | 27 | Activate the plugin in hexo's `_config.yml` like this: 28 | ```yml 29 | filter_optimize: 30 | enable: true 31 | # static resource versioning 32 | versioning: false 33 | css: 34 | # minify all css files 35 | minify: true 36 | excludes: 37 | # use preload to load css elements dynamically 38 | delivery: 39 | - '@fortawesome/fontawesome-free' 40 | - 'fonts.googleapis.com' 41 | # make specific css content inline into the html page 42 | inlines: 43 | # support full path only 44 | - css/main.css 45 | js: 46 | # minify all js files 47 | minify: true 48 | excludes: 49 | # remove the comments in each of the js files 50 | remove_comments: false 51 | html: 52 | # minify all html files 53 | minify: true 54 | excludes: 55 | # set the priority of this plugin, 56 | # lower means it will be executed first, default of Hexo is 10 57 | priority: 12 58 | ``` 59 | 60 | This plugin can be disabled by `NODE_ENV` in development to boost `hexo generate`: 61 | ``` 62 | export NODE_ENV=development 63 | ``` 64 | 65 | ## Comparison 66 | 67 | Here is a result from [GTmetrix](https://gtmetrix.com) to show you the changes between before and after. (Same web server located in Tokyo, Japan, vultr.com) 68 | 69 | * **Remove query strings from static resources** - let all the proxies could cache resources well. (https://gtmetrix.com/remove-query-strings-from-static-resources.html) 70 | * **Make fewer HTTP requests** - through combined the loaded js files into the one. 71 | * **Prefer asynchronous resources** - change the css delivery method using a script block instead of link tag. 72 | * And TODOs ... 73 | 74 | ![Comparison](https://user-images.githubusercontent.com/980449/35233293-a8229c72-ffd8-11e7-8a23-3b8bc10d40c3.png) 75 | 76 | [github-image]: https://img.shields.io/github/actions/workflow/status/next-theme/hexo-optimize/linter.yml?branch=main&style=flat-square 77 | [npm-image]: https://img.shields.io/npm/v/hexo-optimize.svg?style=flat-square 78 | [lic-image]: https://img.shields.io/npm/l/hexo-optimize?style=flat-square 79 | 80 | [size-image]: https://img.shields.io/github/languages/code-size/next-theme/hexo-optimize?style=flat-square 81 | [dm-image]: https://img.shields.io/npm/dm/hexo-optimize?style=flat-square 82 | [dt-image]: https://img.shields.io/npm/dt/hexo-optimize?style=flat-square 83 | 84 | [github-url]: https://github.com/next-theme/hexo-optimize/actions?query=workflow%3ALinter 85 | [npm-url]: https://www.npmjs.com/package/hexo-optimize 86 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const config = require("@next-theme/eslint-config"); 2 | 3 | module.exports = config; 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global hexo */ 2 | 3 | 'use strict'; 4 | 5 | const { deepMerge } = require('hexo-util'); 6 | 7 | hexo.config.filter_optimize = deepMerge({ 8 | enable : true, 9 | versioning: false, 10 | css : { 11 | minify : true, 12 | excludes: [], 13 | delivery: [], 14 | inlines : [] 15 | }, 16 | js: { 17 | minify : true, 18 | excludes : [], 19 | remove_comments: false 20 | }, 21 | html: { 22 | minify : true, 23 | excludes: [] 24 | }, 25 | image: { 26 | minify : true, 27 | interlaced : false, 28 | multipass : false, 29 | optimizationLevel: 2, 30 | pngquant : false, 31 | progressive : false 32 | } 33 | }, hexo.config.filter_optimize); 34 | 35 | const config = hexo.config.filter_optimize; 36 | if (process.env.NODE_ENV !== 'development' && config.enable) { 37 | const { filter, css, js } = require('./lib/index'); 38 | const priority = parseInt(config.priority, 10) || 10; 39 | 40 | // Enable one of the optimizations. 41 | if (config.css.delivery.length || config.css.inlines.length || config.html.minify || config.versioning) { 42 | hexo.extend.filter.register('after_generate', filter, priority); 43 | } 44 | if (config.css.minify) { 45 | hexo.extend.filter.register('after_render:css', css); 46 | } 47 | if (config.js.minify) { 48 | hexo.extend.filter.register('after_render:js', js); 49 | } 50 | if (config.image.minify) { 51 | //hexo.extend.filter.register('after_generate', image); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/css.js: -------------------------------------------------------------------------------- 1 | const css = require('lightningcss'); 2 | const { inExcludes } = require('./util'); 3 | 4 | module.exports = function(str, data) { 5 | const { excludes } = this.config.filter_optimize.css; 6 | if (inExcludes(data.path, excludes)) return str; 7 | return css.transform({ 8 | code : Buffer.from(str), 9 | minify: true 10 | }).code.toString(); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/filter.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird'); 2 | const micromatch = require('micromatch'); 3 | const { URL } = require('url'); 4 | const { join, parse } = require('path'); 5 | const { streamRead, hash } = require('./util'); 6 | const html = require('./html'); 7 | 8 | function minify(str, path, config, { cssFiles, jsMap, cssMap }) { 9 | const { url, root, filter_optimize } = config; 10 | const { inlines, delivery } = filter_optimize.css; 11 | str = str.replace(/]*\shref="([^"]+)"[^>]*>/ig, (match, href) => { 12 | // Exit if the href attribute doesn't exist. 13 | if (!href) return match; 14 | 15 | const link = new URL(href, url); 16 | const originalURL = link.href; 17 | for (const path of inlines) { 18 | if (link.pathname === join(root, path)) { 19 | return ``; 20 | } 21 | } 22 | for (const path in cssMap) { 23 | if (link.pathname === join(root, path)) { 24 | link.pathname = join(root, cssMap[path]); 25 | continue; 26 | } 27 | } 28 | const newURL = link.hostname === new URL(url).hostname ? link.pathname : link.href; 29 | if (delivery.some(pattern => originalURL.includes(pattern))) { 30 | return ``; 31 | } 32 | return match.replace(href, newURL); 33 | }).replace(/]*\ssrc="([^"]+)"[^>]*>/ig, (match, src) => { 34 | // Exit if the src attribute doesn't exist. 35 | if (!src) return match; 36 | 37 | const link = new URL(src, url); 38 | 39 | for (const path in jsMap) { 40 | if (link.pathname === join(root, path)) { 41 | link.pathname = join(root, jsMap[path]); 42 | continue; 43 | } 44 | } 45 | const newURL = link.hostname === new URL(url).hostname ? link.pathname : link.href; 46 | return match.replace(src, newURL); 47 | }); 48 | if (filter_optimize.html.minify) { 49 | return html(str, path, config); 50 | } 51 | return str; 52 | } 53 | 54 | module.exports = async function() { 55 | 56 | const { route, config } = this; 57 | const { inlines } = config.filter_optimize.css; 58 | 59 | const list = route.list(); 60 | const htmls = list.filter(path => micromatch.isMatch(path, '**/*.html', { nocase: true })); 61 | 62 | const cssFiles = {}; 63 | const jsMap = {}; 64 | const cssMap = {}; 65 | 66 | await Promise.map(inlines, async path => { 67 | const str = await streamRead(route, path); 68 | cssFiles[path] = str; 69 | }); 70 | 71 | if (config.filter_optimize.versioning) { 72 | const js = list.filter(path => micromatch.isMatch(path, '**/*.js', { nocase: true })); 73 | const css = list.filter(path => micromatch.isMatch(path, '**/*.css', { nocase: true })); 74 | 75 | await Promise.map(js, async path => { 76 | const str = await streamRead(route, path); 77 | const { dir, name, ext } = parse(path); 78 | const newPath = join(dir, `${name}-${hash(str)}${ext}`); 79 | route.set(newPath, str); 80 | jsMap[path] = newPath; 81 | }); 82 | 83 | await Promise.map(css, async path => { 84 | let str; 85 | if (path in cssFiles) { 86 | str = cssFiles[path]; 87 | } else { 88 | str = await streamRead(route, path); 89 | } 90 | const { dir, name, ext } = parse(path); 91 | const newPath = join(dir, `${name}-${hash(str)}${ext}`); 92 | route.set(newPath, str); 93 | cssMap[path] = newPath; 94 | }); 95 | } 96 | 97 | await Promise.map(htmls, async path => { 98 | let str = await streamRead(route, path); 99 | str = minify(str, path, config, { cssFiles, jsMap, cssMap }); 100 | route.set(path, str); 101 | }); 102 | }; 103 | -------------------------------------------------------------------------------- /lib/html.js: -------------------------------------------------------------------------------- 1 | const minifyHtml = require('@minify-html/node'); 2 | const { inExcludes } = require('./util'); 3 | 4 | module.exports = function(str, path, config) { 5 | const { excludes } = config.filter_optimize.html; 6 | if (!minifyHtml || inExcludes(path, excludes)) return str; 7 | return minifyHtml.minify(Buffer.from(str), { 8 | keep_spaces_between_attributes: true, 9 | keep_comments : true 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const filter = require('./filter'); 2 | const css = require('./css'); 3 | const js = require('./js'); 4 | 5 | // the main filter function 6 | module.exports = { filter, css, js }; 7 | -------------------------------------------------------------------------------- /lib/js.js: -------------------------------------------------------------------------------- 1 | const Terser = require('terser'); 2 | const { inExcludes } = require('./util'); 3 | 4 | module.exports = function(str, data) { 5 | const { excludes, remove_comments } = this.config.filter_optimize.js; 6 | if (inExcludes(data.path, excludes)) return str; 7 | return Terser.minify(str, { 8 | output: { 9 | comments: !remove_comments 10 | } 11 | }).then(res => res.code); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird'); 2 | const micromatch = require('micromatch'); 3 | const crypto = require('crypto'); 4 | 5 | function streamRead(route, path, binary) { 6 | const stream = route.get(path); 7 | if (binary) { 8 | return new Promise((resolve, reject) => { 9 | const arr = []; 10 | stream 11 | .on('data', chunk => arr.push(chunk)) 12 | .on('end', () => resolve(Buffer.concat(arr))) 13 | .on('error', reject); 14 | }); 15 | } 16 | return new Promise((resolve, reject) => { 17 | let data = ''; 18 | stream 19 | .on('data', chunk => { 20 | data += chunk.toString(); 21 | }) 22 | .on('end', () => { 23 | if (data === '') { 24 | // eslint-disable-next-line no-console 25 | console.error(`File ${path} is empty. Please check your hexo-optimize config.`); 26 | } 27 | resolve(data); 28 | }) 29 | .on('error', reject); 30 | }); 31 | } 32 | 33 | // check whether `path` is in `excludes` 34 | function inExcludes(path, excludes) { 35 | return excludes && excludes.some(item => micromatch.isMatch(path, item, { nocase: true })); 36 | } 37 | 38 | function hash(str) { 39 | return crypto.createHash('sha256').update(str).digest('hex').substring(0, 8); 40 | } 41 | 42 | module.exports = { streamRead, inExcludes, hash }; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-optimize", 3 | "version": "3.4.0", 4 | "description": "A hexo plugin that optimize the pages loading speed.", 5 | "main": "index.js", 6 | "files": [ 7 | "src/**", 8 | "Cargo.toml", 9 | "lib", 10 | "index.js", 11 | "postinstall.js" 12 | ], 13 | "scripts": { 14 | "lint": "eslint index.js lib" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/next-theme/hexo-optimize.git" 19 | }, 20 | "keywords": [ 21 | "hexo", 22 | "NexT", 23 | "plugins", 24 | "optimization" 25 | ], 26 | "author": "Tsanie Lily", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/next-theme/hexo-optimize/issues" 30 | }, 31 | "homepage": "https://github.com/next-theme/hexo-optimize#readme", 32 | "dependencies": { 33 | "@minify-html/node": "^0.16.4", 34 | "bluebird": "^3.7.2", 35 | "lightningcss": "^1.24.1", 36 | "micromatch": "^4.0.5", 37 | "terser": "^5.30.3" 38 | }, 39 | "devDependencies": { 40 | "@next-theme/eslint-config": "0.0.4", 41 | "eslint": "^9.23.0", 42 | "lighthouse": "^12.5.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ============================================================== # 3 | # Shell script to autodeploy Hexo & NexT & NexT website source. 4 | # ============================================================== # 5 | PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin:$PATH 6 | export PATH 7 | 8 | # https://en.wikipedia.org/wiki/ANSI_escape_code 9 | #red='\033[0;31m' 10 | #green='\033[0;32m' 11 | #brown='\033[0;33m' 12 | #blue='\033[0;34m' 13 | #purple='\033[0;35m' 14 | cyan='\033[0;36m' 15 | #lgray='\033[0;37m' 16 | #dgray='\033[1;30m' 17 | lred='\033[1;31m' 18 | lgreen='\033[1;32m' 19 | yellow='\033[1;33m' 20 | lblue='\033[1;34m' 21 | lpurple='\033[1;35m' 22 | lcyan='\033[1;36m' 23 | white='\033[1;37m' 24 | norm='\033[0m' 25 | bold='\033[1m' 26 | 27 | echo 28 | echo "==============================================================" 29 | echo " ${yellow}Checking starting directory structure...${norm}" 30 | echo "==============================================================" 31 | echo "${lcyan}`pwd`${norm}" 32 | du -sh 33 | du -sh * 34 | 35 | echo 36 | echo "==============================================================" 37 | echo " ${lgreen}Checking Node.js & NPM version...${norm}" 38 | echo "==============================================================" 39 | echo "${yellow}Node version:${norm} ${lcyan}`node -v`${norm}" 40 | echo "${yellow}NPM version:${norm} ${lcyan}`npm -v`${norm}" 41 | 42 | echo 43 | echo "==============================================================" 44 | echo " ${lgreen}Installing Hexo & NPM modules...${norm}" 45 | echo "==============================================================" 46 | rm -rf .tmp-hexo-optimize-test 47 | git clone https://github.com/hexojs/hexo-theme-unit-test .tmp-hexo-optimize-test 48 | cd .tmp-hexo-optimize-test 49 | npm install --silent 50 | npm install hexo-theme-next hexo-optimize 51 | 52 | echo 53 | echo "==============================================================" 54 | echo " ${lgreen}Edit config file...${norm}" 55 | echo "==============================================================" 56 | echo " 57 | filter_optimize: 58 | enable: true 59 | css: 60 | # minify all css files 61 | minify: false 62 | excludes: 63 | # use preload to load css elements dynamically 64 | delivery: 65 | - 'font-awesome' 66 | - 'fonts.googleapis.com' 67 | # make specific css content inline into the html page 68 | inlines: 69 | # support full path only 70 | - css/main.css 71 | js: 72 | # minify all js files 73 | minify: false 74 | excludes: 75 | # remove the comments in each of the js files 76 | remove_comments: false 77 | html: 78 | # minify all html files 79 | minify: false 80 | excludes: 81 | # set the priority of this plugin, 82 | # lower means it will be executed first, default of Hexo is 10 83 | priority: 12" >> _config.yml 84 | 85 | echo 86 | echo "==============================================================" 87 | echo " ${yellow}Checking Hexo version...${norm}" 88 | echo "==============================================================" 89 | hexo() { 90 | npx hexo "$@" 91 | } 92 | hexo -v 93 | npm ls --depth 0 94 | hexo config theme next 95 | hexo config theme_config.motion.enable false 96 | hexo config theme_config.font.enable true 97 | cp _config.yml _config.yml.bak 98 | 99 | echo 100 | echo "==============================================================" 101 | echo " ${lpurple}Generating content for All Optimization...${norm}" 102 | echo "==============================================================" 103 | hexo config url https://hexo-optimize.netlify.app/all 104 | hexo clean && hexo g 105 | 106 | echo "${lred}`mv -v public all`${norm}" 107 | 108 | echo 109 | echo "==============================================================" 110 | echo " ${lpurple}Generating content for CSS Delivery...${norm}" 111 | echo "==============================================================" 112 | hexo config url https://hexo-optimize.netlify.app/delivery 113 | hexo config filter_optimize.css.inlines null 114 | sed -i 's/inlines: null//g' _config.yml 115 | hexo clean && hexo g 116 | 117 | echo "${lred}`mv -v public delivery`${norm}" 118 | 119 | echo 120 | echo "==============================================================" 121 | echo " ${lpurple}Generating content for CSS Inlines...${norm}" 122 | echo "==============================================================" 123 | cp _config.yml.bak _config.yml 124 | hexo config url https://hexo-optimize.netlify.app/inlines 125 | hexo config filter_optimize.css.delivery null 126 | sed -i 's/delivery: null//g' _config.yml 127 | hexo clean && hexo g 128 | 129 | echo "${lred}`mv -v public inlines`${norm}" 130 | 131 | echo 132 | echo "==============================================================" 133 | echo " ${lpurple}Generating content for None Optimization...${norm}" 134 | echo "==============================================================" 135 | hexo config url https://hexo-optimize.netlify.app/none 136 | hexo config filter_optimize.css.inlines null 137 | sed -i 's/inlines: null//g' _config.yml 138 | hexo clean && hexo g 139 | 140 | echo "${lred}`mv -v public none`${norm}" 141 | 142 | echo 143 | echo "==============================================================" 144 | echo " ${lpurple}Moving all schemes to public directory...${norm}" 145 | echo "==============================================================" 146 | mkdir public 147 | echo "${lred}`mv -v all delivery inlines none -t public`${norm}" 148 | 149 | echo "${yellow}robots.txt:${norm}" 150 | echo "User-agent: * 151 | Disallow: /* 152 | Host: https://hexo-optimize.netlify.app" > public/robots.txt 153 | cat public/robots.txt 154 | mv public ../ 155 | 156 | echo 157 | echo "==============================================================" 158 | echo " ${yellow}Checking 'repo' directory structure...${norm}" 159 | echo "==============================================================" 160 | echo "${lcyan}`pwd`${norm}" 161 | du -sh 162 | du -sh * 163 | 164 | echo 165 | echo "==============================================================" 166 | echo " ${yellow}Checking 'public' directory structure...${norm}" 167 | echo "==============================================================" 168 | cd ../public 169 | echo "${lcyan}`pwd`${norm}" 170 | du -sh 171 | du -sh * 172 | 173 | echo 174 | echo "==============================================================" 175 | echo " ${lgreen}Done. Beginning to deploy site...${norm}" 176 | echo "==============================================================" 177 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const lighthouse = require('lighthouse'); 2 | const chromeLauncher = require('chrome-launcher'); 3 | 4 | function launchChromeAndRunLighthouse(url, flags = {}, config = null) { 5 | return chromeLauncher.launch(flags).then(chrome => { 6 | flags.port = chrome.port; 7 | return lighthouse(url, flags, config).then(results => 8 | chrome.kill().then(() => results)); 9 | }); 10 | } 11 | 12 | const flags = { 13 | chromeFlags: ['--headless'] 14 | }; 15 | 16 | launchChromeAndRunLighthouse('https://www.baidu.com', flags).then(results => { 17 | // Use results! 18 | }); 19 | --------------------------------------------------------------------------------