├── .node-version ├── pnpm-workspace.yaml ├── src ├── index.ts ├── unescape.ts ├── unescape.test.ts ├── escape.ts └── escape.test.ts ├── eslint.config.js ├── .nycrc ├── tsconfig.json ├── LICENSE ├── .github └── workflows │ └── publish.yml ├── benchmark ├── index.ts └── fixtures │ └── skk.moe.html ├── package.json ├── .gitignore └── README.md /.node-version: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - '@swc/core' 3 | - unrs-resolver 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { escapeHTML } from './escape'; 2 | export { unescapeHTML } from './unescape'; 3 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('eslint-config-sukka').sukka( 4 | {}, 5 | { 6 | files: ['benchmark/index.ts'], 7 | linterOptions: { 8 | reportUnusedDisableDirectives: false 9 | } 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "check-coverage": true, 4 | "extension": [".ts", ".tsx"], 5 | "all": true, 6 | "include": [ 7 | "src/**/*.[tj]s?(x)" 8 | ], 9 | "exclude": [ 10 | "src/**/*.bench.[tj]s?(x)", 11 | "src/**/*.test.[tj]s?(x)" 12 | ], 13 | "reporter": [ 14 | "lcov", 15 | "text", 16 | "text-summary" 17 | ], 18 | "report-dir": "coverage" 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "lib": ["ES2022"], 5 | "module": "preserve", 6 | "moduleResolution": "bundler", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | }, 10 | "resolveJsonModule": true, 11 | "declaration": true, 12 | "outDir": "./dist", 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "strictNullChecks": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src/**/*.ts", "src/**/*.tsx", "benchmark/**/*.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /src/unescape.ts: -------------------------------------------------------------------------------- 1 | const reHtmlEntityGlobal = /&(?:[gl]t|quot|#39|amp|#6[02]|#34|apos|#38);/g; 2 | const unescapeEntityMap: Record = Object.create(null); 3 | 4 | unescapeEntityMap['<'] = '<'; 5 | unescapeEntityMap['>'] = '>'; 6 | unescapeEntityMap['"'] = '"'; 7 | unescapeEntityMap['''] = '\''; 8 | unescapeEntityMap['&'] = '&'; 9 | unescapeEntityMap['<'] = '<'; 10 | unescapeEntityMap['>'] = '>'; 11 | unescapeEntityMap['"'] = '"'; 12 | unescapeEntityMap['''] = '\''; 13 | unescapeEntityMap['&'] = '&'; 14 | 15 | const replacer = (match: string) => unescapeEntityMap[match]; 16 | 17 | // eslint-disable-next-line @typescript-eslint/unbound-method -- cache prototype look up 18 | const StringPrototypeReplace = String.prototype.replace; 19 | 20 | export function unescapeHTML(str: string) { 21 | return StringPrototypeReplace.call(str, reHtmlEntityGlobal, replacer); 22 | } 23 | -------------------------------------------------------------------------------- /src/unescape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | import { expect } from 'expect'; 3 | import { unescapeHTML } from '.'; 4 | 5 | describe('unescape', () => { 6 | it('not html', () => { 7 | expect(unescapeHTML('https://skk.moe')).toStrictEqual('https://skk.moe'); 8 | }); 9 | 10 | it('correct unescape', () => { 11 | expect(unescapeHTML('&<>'"<'')).toStrictEqual('&<>\'"<\''); 12 | }); 13 | 14 | it('mixed', () => { 15 | expect(unescapeHTML('<p class="foo">Hello "world".</p>')).toStrictEqual('

Hello "world".

'); 16 | }); 17 | 18 | it('wrong sequence', () => { 19 | const fixture = '&&|&;;|&ll|&rr|&tt|&aa;|&amq;|&mm|&apo;|&m|&3|&u|&8|&6|&0'; 20 | expect(unescapeHTML('<' + fixture)).toStrictEqual('<' + fixture); 21 | }); 22 | 23 | it('no potential XSS', () => { 24 | const unescaped = unescapeHTML('&lt;script&gt;alert("yo")&lt;/script&gt;'); 25 | expect(unescaped).not.toContain(''); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Sukka 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 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Publish 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags-ignore: 7 | - '**' 8 | paths-ignore: 9 | - '**/*.md' 10 | - LICENSE 11 | - '**/*.gitignore' 12 | - .editorconfig 13 | - docs/** 14 | pull_request: null 15 | jobs: 16 | publish: 17 | name: Publish 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | id-token: write 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: pnpm/action-setup@v4 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version-file: '.node-version' 28 | check-latest: true 29 | cache: 'pnpm' 30 | registry-url: 'https://registry.npmjs.org' 31 | - run: npm install -g npm 32 | - run: pnpm install 33 | - run: pnpm run lint 34 | - run: pnpm run test 35 | - run: pnpm run build 36 | - name: Publish 37 | run: | 38 | if git log -1 --pretty=%B | grep "^release: [0-9]\+\.[0-9]\+\.[0-9]\+$"; 39 | then 40 | pnpm publish -r --provenance --access public 41 | elif git log -1 --pretty=%B | grep "^release: [0-9]\+\.[0-9]\+\.[0-9]\+"; 42 | then 43 | pnpm publish -r --provenance --access public --tag next 44 | else 45 | echo "Not a release, skipping publish" 46 | fi 47 | env: 48 | NPM_CONFIG_PROVENANCE: true 49 | -------------------------------------------------------------------------------- /src/escape.ts: -------------------------------------------------------------------------------- 1 | const reHtmlEntity = /["&'<>]/; 2 | export function escapeHTML(str: string) { 3 | const match = reHtmlEntity.exec(str); 4 | 5 | if (match === null) { // faster than !match since no type conversion 6 | return str; 7 | } 8 | 9 | let escape = ''; 10 | let html = ''; 11 | 12 | let index = match.index; 13 | let lastIndex = 0; 14 | const len = str.length; 15 | 16 | // iterate from the first match 17 | 18 | /** 19 | * Adjust order for commonly seen symbols: 20 | * Take https://tc39.es/ecma262 as an example, 21 | * 22 | * < 369266 23 | * > 369296, though sometimes it is more common, it alomost always comes after < 24 | * " 277105, people seems always prefer " over ' in HTML 25 | * ' 484, less often, but sometimes is used for attribute anyway 26 | * & 4424 27 | */ 28 | 29 | for (; index < len; index++) { 30 | switch (str.charCodeAt(index)) { 31 | case 60: // < 32 | escape = '<'; 33 | break; 34 | case 62: // > 35 | escape = '>'; 36 | break; 37 | case 34: // " 38 | escape = '"'; 39 | break; 40 | case 39: // ' 41 | escape = '''; 42 | break; 43 | case 38: // & 44 | escape = '&'; 45 | break; 46 | default: 47 | continue; 48 | } 49 | 50 | if (lastIndex !== index) { 51 | html += str.slice(lastIndex, index); 52 | } 53 | html += escape; 54 | 55 | lastIndex = index + 1; 56 | } 57 | 58 | if (lastIndex !== index) { 59 | html += str.slice(lastIndex, index); 60 | } 61 | 62 | return html; 63 | } 64 | -------------------------------------------------------------------------------- /src/escape.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | import { expect } from 'expect'; 3 | import { escapeHTML } from '.'; 4 | 5 | describe('escape', () => { 6 | it('not html', () => { 7 | expect(escapeHTML('https://skk.moe')).toStrictEqual('https://skk.moe'); 8 | }); 9 | 10 | it('when string contains \'"\'', () => { 11 | expect(escapeHTML('"')).toStrictEqual('"'); 12 | expect(escapeHTML('"bar')).toStrictEqual('"bar'); 13 | expect(escapeHTML('foo"')).toStrictEqual('foo"'); 14 | expect(escapeHTML('foo"bar')).toStrictEqual('foo"bar'); 15 | expect(escapeHTML('foo""bar')).toStrictEqual('foo""bar'); 16 | }); 17 | 18 | it('when string contains "&"', () => { 19 | expect(escapeHTML('&')).toStrictEqual('&'); 20 | expect(escapeHTML('&bar')).toStrictEqual('&bar'); 21 | expect(escapeHTML('foo&')).toStrictEqual('foo&'); 22 | expect(escapeHTML('foo&bar')).toStrictEqual('foo&bar'); 23 | expect(escapeHTML('foo&&bar')).toStrictEqual('foo&&bar'); 24 | }); 25 | 26 | it('when string contains "\'"', () => { 27 | expect(escapeHTML('\'')).toStrictEqual('''); 28 | expect(escapeHTML('\'bar')).toStrictEqual(''bar'); 29 | expect(escapeHTML('foo\'')).toStrictEqual('foo''); 30 | expect(escapeHTML('foo\'bar')).toStrictEqual('foo'bar'); 31 | expect(escapeHTML('foo\'\'bar')).toStrictEqual('foo''bar'); 32 | }); 33 | 34 | it('when string contains "<"', () => { 35 | expect(escapeHTML('<')).toStrictEqual('<'); 36 | expect(escapeHTML('"', () => { 43 | expect(escapeHTML('>')).toStrictEqual('>'); 44 | expect(escapeHTML('>bar')).toStrictEqual('>bar'); 45 | expect(escapeHTML('foo>')).toStrictEqual('foo>'); 46 | expect(escapeHTML('foo>bar')).toStrictEqual('foo>bar'); 47 | expect(escapeHTML('foo>>bar')).toStrictEqual('foo>>bar'); 48 | }); 49 | 50 | it('when escaped character mixed', () => { 51 | expect(escapeHTML('&foo <> bar "fizz" l\'a')).toStrictEqual('&foo <> bar "fizz" l'a'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /benchmark/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-restricted-imports -- benchmark */ 2 | import path from 'node:path'; 3 | import fs from 'node:fs'; 4 | 5 | import npmEscapeHtml from 'escape-html'; 6 | import { escape as htmlEscaper, unescape as htmlEscaperUnescape } from 'html-escaper'; 7 | import { htmlEscape as escapeGoat } from 'escape-goat'; 8 | import lodashEscape from 'lodash.escape'; 9 | import loadshUnescape from 'lodash.unescape'; 10 | import { escapeHTML as escapeHTMLRs } from '@napi-rs/escape'; 11 | import { unescapeHTML as hexoUtilUnescapeHtml } from 'hexo-util'; 12 | 13 | // import npmUnescapeHtml from 'unescape-html'; // not safe 14 | 15 | // @ts-expect-error -- no types 16 | import npmUnescape from 'unescape'; 17 | 18 | (async () => { 19 | const { bench, group, run } = await import('mitata'); 20 | // eslint-disable-next-line import-x/no-unresolved -- only exist after build 21 | const { escapeHTML, unescapeHTML } = await import('../dist/es/index.mjs'); 22 | 23 | const escapeFns = [ 24 | ['fast-escape-html', escapeHTML], 25 | ['escape-html', npmEscapeHtml], 26 | ['@napi-rs/escape', escapeHTMLRs], 27 | ['html-escaper', htmlEscaper], 28 | ['lodash.escape', lodashEscape], 29 | ['escape-goat', escapeGoat] 30 | ] as const; 31 | const unescapeFns = [ 32 | ['fast-escape-html', unescapeHTML], 33 | ['html-escaper', htmlEscaperUnescape], 34 | ['lodash.unescape', loadshUnescape], 35 | ['hexo-util', hexoUtilUnescapeHtml], 36 | ['unescape', npmUnescape] 37 | ] as const; 38 | 39 | const fixtures = [ 40 | ['skk.moe', 'skk.moe.html'], 41 | ['github.com (incognito)', 'github.com.html'], 42 | ['stackoverflow.com (incognito)', 'stackoverflow.com.html'], 43 | ['www.google.com (incognito)', 'google.com.html'], 44 | ['about.gitlab.com', 'about.gitlab.com.html'] 45 | ] as const; 46 | 47 | group('escape', () => { 48 | fixtures.forEach(([name, fixturePath]) => { 49 | group(name, () => { 50 | const fixture = fs.readFileSync(path.join(__dirname, './fixtures/', fixturePath), 'utf-8'); 51 | escapeFns.forEach(([name, fn]) => { 52 | bench(name, () => fn(fixture)); 53 | }); 54 | }); 55 | }); 56 | }); 57 | 58 | group('unescape', () => { 59 | fixtures.forEach(([name, fixturePath]) => { 60 | const fixture = fs.readFileSync(path.join(__dirname, './fixtures/', fixturePath), 'utf-8'); 61 | const escaped = escapeHTML(fixture); 62 | 63 | group(name, () => { 64 | unescapeFns.forEach(([name, fn]) => { 65 | bench(name, () => fn(escaped)); 66 | }); 67 | }); 68 | }); 69 | }); 70 | 71 | run(); 72 | })(); 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-escape-html", 3 | "version": "1.1.1-beta.2", 4 | "description": "Fastest HTML escape on Node.js, even faster than the Rust-based one!", 5 | "repository": { 6 | "url": "https://github.com/SukkaW/fast-escape-html" 7 | }, 8 | "main": "./dist/cjs/index.js", 9 | "module": "./dist/es/index.mjs", 10 | "types": "./dist/cjs/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "exports": { 15 | ".": { 16 | "import": { 17 | "types": "./dist/es/index.d.mts", 18 | "default": "./dist/es/index.mjs" 19 | }, 20 | "require": { 21 | "types": "./dist/cjs/index.d.ts", 22 | "default": "./dist/cjs/index.js" 23 | } 24 | } 25 | }, 26 | "scripts": { 27 | "lint": "eslint --format=sukka .", 28 | "test": "SWC_NODE_IGNORE_DYNAMIC=1 SWC_NODE_INLINE_SOURCE_MAP=1 nyc mocha --require @swc-node/register --full-trace ./src/**/*.test.ts", 29 | "bench": "SWC_NODE_IGNORE_DYNAMIC=1 node --require @swc-node/register benchmark/index.ts", 30 | "build": "bunchee --no-sourcemap", 31 | "prerelease": "pnpm run lint && pnpm run test && pnpm run build", 32 | "release": "bumpp -r --all --commit \"release: %s\" --tag \"%s\"" 33 | }, 34 | "keywords": [ 35 | "escape", 36 | "escape-html", 37 | "utility", 38 | "escape-goat" 39 | ], 40 | "author": "SukkaW ", 41 | "license": "MIT", 42 | "devDependencies": { 43 | "@eslint-sukka/node": "^7.1.1", 44 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 45 | "@mitata/counters": "^0.0.8", 46 | "@napi-rs/escape": "^1.0.1", 47 | "@swc-node/register": "^1.11.1", 48 | "@types/escape-html": "^1.0.4", 49 | "@types/html-escaper": "^3.0.4", 50 | "@types/lodash.escape": "^4.0.9", 51 | "@types/lodash.unescape": "^4.0.9", 52 | "@types/mocha": "^10.0.10", 53 | "@types/node": "^22.18.6", 54 | "bumpp": "^10.2.3", 55 | "bunchee": "^6.6.0", 56 | "escape-goat": "^4.0.0", 57 | "escape-html": "^1.0.3", 58 | "eslint": "^9.36.0", 59 | "eslint-config-sukka": "^7.1.1", 60 | "eslint-formatter-sukka": "^7.1.1", 61 | "expect": "30.0.0-beta.3", 62 | "hexo-util": "^3.3.0", 63 | "html-escaper": "^3.0.3", 64 | "lodash.escape": "^4.0.1", 65 | "lodash.unescape": "^4.0.1", 66 | "mitata": "^1.0.34", 67 | "mocha": "^11.7.2", 68 | "nyc": "^17.1.0", 69 | "typescript": "^5.9.2", 70 | "unescape": "^1.0.1" 71 | }, 72 | "packageManager": "pnpm@10.17.1", 73 | "pnpm": { 74 | "overrides": { 75 | "eslint>chalk": "npm:picocolors@^1.1.1" 76 | }, 77 | "onlyBuiltDependencies": [ 78 | "@swc/core", 79 | "oxc-resolver", 80 | "unrs-resolver" 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .DS_Store 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-escape-html 2 | 3 | **Fastest**, zero-dependencies, plain JavaScript-based, 100% test coverage, HTML escaping library for JavaScript, works in both Node.js and browser. 4 | 5 | Even faster than the Rust-based `@napi-rs/escape` with real world HTMLs (see benchmark below)! 6 | 7 | ## Installation 8 | 9 | ```bash 10 | # Using npm, yarn, or pnpm 11 | npm install fast-escape-html 12 | yarn add fast-escape-html 13 | pnpm add fast-escape-html 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```ts 19 | import { escapeHTML, unescapeHTML } from 'fast-escape-html'; 20 | ``` 21 | 22 | ## **DO NOT USE `unescape-html` OR `escape-goat` TO UNESCAPE UN-TRUSTED HTML!** 23 | 24 | Only use safe libraries like `fast-escape-html` or `html-escaper` that only look up and replace the entire input once. 25 | 26 | Both `unescape-html` and `escape-goat` use the **multiple replace** implementation similar to this: 27 | 28 | ```ts 29 | function unescapeHTML(str: string): string { 30 | return str 31 | .replace(/&/g, '&') 32 | .replace(/</g, '<') 33 | .replace(/>/g, '>') 34 | .replace(/"/g, '"') 35 | .replace(/'/g, "'"); 36 | } 37 | ``` 38 | 39 | BUT THAT IS **UNSAFE**! Considering this **untrusted input**: 40 | 41 | ```html 42 | &lt;script&gt;alert("yo")&lt;/script&gt; 43 | ``` 44 | 45 | With `fast-escape-html` or `html-escaper` it will become: 46 | 47 | ```html 48 | <script>alert("yo")</script> 49 | ``` 50 | 51 | Which is safe. But with `unescape-html` or `escape-goat`, it will become: 52 | 53 | ```html 54 | 55 | ``` 56 | 57 | Boom, XSS! 58 | 59 | ## Benchmark 60 | 61 | The benchmark uses [`mitata`](https://www.npmjs.com/package/mitata) against realworld websites' HTMLs: 62 | 63 | - https://skk.moe 64 | - https://github.com (in incognito mode) 65 | - https://stackoverflow.com/questions (in incognito mode) 66 | - https://www.google.com (in incognito mode) 67 | 68 | ```bash 69 | # Before running the benchmark, build the dist 70 | # The benchmark cases are run against the built files instead of the source files 71 | pnpm i && pnpm run build 72 | 73 | # Run the benchmark 74 | pnpm run bench 75 | # On supported platforms, you can use "sudo" to enable hardware counter 76 | # https://github.com/evanwashere/mitata#hardware-counters 77 | sudo pnpm run bench 78 | ``` 79 | 80 | ### Escape 81 | 82 | ``` 83 | clk: ~3.17 GHz 84 | cpu: Apple M2 Max 85 | runtime: node 22.15.1 (arm64-darwin) 86 | 87 | benchmark avg (min … max) p75 / p99 (min … top 1%) 88 | ------------------------------------------- ------------------------------- 89 | • skk.moe 90 | ------------------------------------------- ------------------------------- 91 | fast-escape-html 227.50 µs/iter 229.71 µs █ 92 | (208.46 µs … 430.29 µs) 359.75 µs █▆ 93 | (100.02 kb … 883.06 kb) 485.43 kb ████▅▃▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁ 94 | 4.64 ipc ( 0.87% stalls) 99.35% L1 data cache 95 | 777.26k cycles 3.61M instructions 21.15% retired LD/ST (763.48k) 96 | 97 | escape-html 231.02 µs/iter 235.46 µs █ 98 | (211.71 µs … 488.08 µs) 380.33 µs █ 99 | ( 35.66 kb … 723.82 kb) 484.97 kb ███▆▆▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 100 | 4.60 ipc ( 1.02% stalls) 99.12% L1 data cache 101 | 782.60k cycles 3.60M instructions 21.07% retired LD/ST (759.07k) 102 | 103 | @napi-rs/escape 262.46 µs/iter 263.04 µs █ 104 | (226.17 µs … 1.55 ms) 633.96 µs ██ 105 | (207.75 kb … 207.77 kb) 207.77 kb ██▇▇▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 106 | 4.52 ipc ( 16.84% stalls) 83.70% L1 data cache 107 | 873.05k cycles 3.95M instructions 23.64% retired LD/ST (933.00k) 108 | 109 | html-escaper 385.23 µs/iter 385.38 µs █ 110 | (347.17 µs … 963.50 µs) 660.58 µs ██ 111 | (508.75 kb … 576.19 kb) 571.14 kb ▅███▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 112 | 4.23 ipc ( 3.01% stalls) 97.82% L1 data cache 113 | 1.31M cycles 5.53M instructions 34.01% retired LD/ST ( 1.88M) 114 | 115 | lodash.escape 389.55 µs/iter 388.08 µs █ 116 | (353.88 µs … 1.47 ms) 813.17 µs █ 117 | (468.27 kb … 576.36 kb) 574.02 kb ███▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 118 | 4.24 ipc ( 2.95% stalls) 97.89% L1 data cache 119 | 1.33M cycles 5.64M instructions 34.09% retired LD/ST ( 1.92M) 120 | 121 | escape-goat 506.45 µs/iter 499.46 µs █ 122 | (454.54 µs … 1.29 ms) 1.05 ms █ 123 | ( 1.22 mb … 1.22 mb) 1.22 mb ███▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 124 | 3.60 ipc ( 2.92% stalls) 97.54% L1 data cache 125 | 1.72M cycles 6.21M instructions 30.87% retired LD/ST ( 1.92M) 126 | 127 | • github.com (incognito) 128 | ------------------------------------------- ------------------------------- 129 | fast-escape-html 764.52 µs/iter 766.75 µs █ 130 | (708.88 µs … 1.17 ms) 985.13 µs ▇█▇▅ 131 | ( 1.19 mb … 1.19 mb) 1.19 mb ▃████▇▄▂▁▁▁▁▁▁▁▂▃▂▂▂▁ 132 | 4.62 ipc ( 0.70% stalls) 99.50% L1 data cache 133 | 2.61M cycles 12.04M instructions 20.30% retired LD/ST ( 2.45M) 134 | 135 | escape-html 771.81 µs/iter 777.13 µs █▇▃ 136 | (717.63 µs … 1.17 ms) 1.04 ms ████▅ 137 | ( 1.19 mb … 1.19 mb) 1.19 mb █████▆▄▂▂▁▁▁▂▁▃▂▂▂▁▁▂ 138 | 4.56 ipc ( 0.80% stalls) 99.31% L1 data cache 139 | 2.65M cycles 12.06M instructions 20.16% retired LD/ST ( 2.43M) 140 | 141 | @napi-rs/escape 878.95 µs/iter 899.88 µs ▆▆█▇ 142 | (793.75 µs … 1.32 ms) 1.07 ms ▃████▇▅ 143 | (697.81 kb … 697.81 kb) 697.81 kb ▅█████████▇▅▄▂▃▂▁▁▁▁▁ 144 | 4.73 ipc ( 19.05% stalls) 82.51% L1 data cache 145 | 2.95M cycles 13.95M instructions 23.44% retired LD/ST ( 3.27M) 146 | 147 | html-escaper 1.24 ms/iter 1.23 ms █ 148 | (1.04 ms … 4.10 ms) 2.79 ms █ 149 | ( 1.37 mb … 1.43 mb) 1.39 mb ██▇▄▂▁▂▁▂▂▁▁▁▁▁▁▁▁▁▁▁ 150 | 3.92 ipc ( 2.50% stalls) 98.03% L1 data cache 151 | 3.95M cycles 15.45M instructions 33.35% retired LD/ST ( 5.15M) 152 | 153 | lodash.escape 1.16 ms/iter 1.16 ms ▂█▃ 154 | (1.04 ms … 1.88 ms) 1.75 ms ███ 155 | ( 1.37 mb … 1.43 mb) 1.39 mb ▅████▃▂▁▁▁▁▂▁▂▁▂▂▁▂▁▁ 156 | 4.02 ipc ( 2.52% stalls) 98.06% L1 data cache 157 | 3.90M cycles 15.65M instructions 33.64% retired LD/ST ( 5.27M) 158 | 159 | escape-goat 1.34 ms/iter 1.32 ms █▅ 160 | (1.18 ms … 2.18 ms) 1.93 ms ███ 161 | ( 3.82 mb … 3.82 mb) 3.82 mb ▄███▇▃▂▁▁▁▂▃▂▂▂▃▃▂▂▁▁ 162 | 3.58 ipc ( 3.40% stalls) 97.29% L1 data cache 163 | 4.55M cycles 16.29M instructions 30.53% retired LD/ST ( 4.97M) 164 | 165 | • stackoverflow.com (incognito) 166 | ------------------------------------------- ------------------------------- 167 | fast-escape-html 681.29 µs/iter 677.21 µs █▅ 168 | (611.54 µs … 1.89 ms) 1.04 ms ▃██▂ 169 | ( 1.18 mb … 1.18 mb) 1.18 mb ████▅▃▂▂▂▁▁▁▁▂▂▂▂▂▁▁▁ 170 | 4.42 ipc ( 0.84% stalls) 99.35% L1 data cache 171 | 2.30M cycles 10.16M instructions 21.19% retired LD/ST ( 2.15M) 172 | 173 | escape-html 734.02 µs/iter 704.79 µs █ 174 | (621.92 µs … 3.83 ms) 1.81 ms ▅█ 175 | ( 1.18 mb … 1.18 mb) 1.18 mb ██▄▂▂▂▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁ 176 | 4.31 ipc ( 0.94% stalls) 99.13% L1 data cache 177 | 2.36M cycles 10.19M instructions 21.02% retired LD/ST ( 2.14M) 178 | 179 | @napi-rs/escape 817.10 µs/iter 819.04 µs █▅ 180 | (690.96 µs … 2.30 ms) 1.47 ms ███ 181 | (586.23 kb … 586.23 kb) 586.23 kb ▃████▃▂▂▂▂▁▁▁▁▁▁▁▁▂▁▁ 182 | 4.34 ipc ( 16.78% stalls) 82.95% L1 data cache 183 | 2.56M cycles 11.11M instructions 23.36% retired LD/ST ( 2.59M) 184 | 185 | html-escaper 1.12 ms/iter 1.12 ms █▄ 186 | (1.00 ms … 1.80 ms) 1.60 ms ▆██ 187 | ( 1.23 mb … 1.32 mb) 1.28 mb ▆████▅▃▂▂▂▃▂▁▂▂▃▂▂▁▁▁ 188 | 3.84 ipc ( 2.45% stalls) 98.07% L1 data cache 189 | 3.78M cycles 14.50M instructions 33.51% retired LD/ST ( 4.86M) 190 | 191 | lodash.escape 1.11 ms/iter 1.12 ms ▄█▃ 192 | (1.01 ms … 1.84 ms) 1.64 ms ███▃ 193 | ( 1.18 mb … 1.35 mb) 1.28 mb ▆████▄▂▁▁▁▁▁▁▁▂▂▁▂▁▂▁ 194 | 3.92 ipc ( 2.42% stalls) 98.13% L1 data cache 195 | 3.76M cycles 14.75M instructions 33.73% retired LD/ST ( 4.97M) 196 | 197 | escape-goat 1.31 ms/iter 1.25 ms █ 198 | (1.10 ms … 2.44 ms) 2.28 ms ██ 199 | ( 2.82 mb … 3.30 mb) 3.30 mb ▅██▇▃▁▁▁▁▁▁▁▁▁▂▂▂▂▂▁▁ 200 | 3.47 ipc ( 2.78% stalls) 97.73% L1 data cache 201 | 4.40M cycles 15.27M instructions 31.26% retired LD/ST ( 4.77M) 202 | 203 | • www.google.com (incognito) 204 | ------------------------------------------- ------------------------------- 205 | fast-escape-html 400.52 µs/iter 403.25 µs ▄█ 206 | (371.13 µs … 683.21 µs) 591.13 µs ██▇ 207 | (513.24 kb … 513.29 kb) 513.27 kb █████▃▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁ 208 | 4.81 ipc ( 0.80% stalls) 99.13% L1 data cache 209 | 1.36M cycles 6.53M instructions 19.21% retired LD/ST ( 1.25M) 210 | 211 | escape-html 403.63 µs/iter 406.29 µs █▇ 212 | (374.63 µs … 800.79 µs) 616.50 µs ███▅ 213 | (513.24 kb … 513.29 kb) 513.27 kb ████▅▃▂▁▁▁▁▁▁▁▁▁▁▁▂▁▁ 214 | 4.78 ipc ( 0.86% stalls) 98.99% L1 data cache 215 | 1.37M cycles 6.53M instructions 19.10% retired LD/ST ( 1.25M) 216 | 217 | @napi-rs/escape 437.08 µs/iter 458.08 µs █▅▂ 218 | (389.13 µs … 882.75 µs) 527.63 µs ████▆▂ ▄██▄▂ 219 | (390.05 kb … 390.05 kb) 390.05 kb ▅█████████████▅▃▃▃▂▂▁ 220 | 4.70 ipc ( 17.04% stalls) 82.80% L1 data cache 221 | 1.47M cycles 6.91M instructions 22.04% retired LD/ST ( 1.52M) 222 | 223 | html-escaper 514.35 µs/iter 515.21 µs █▅ 224 | (454.92 µs … 1.06 ms) 784.38 µs ▆██▄ 225 | (702.80 kb … 761.38 kb) 758.79 kb ▂████▅▂▁▁▁▁▁▁▁▂▂▁▁▁▁▁ 226 | 4.09 ipc ( 3.12% stalls) 97.59% L1 data cache 227 | 1.72M cycles 7.06M instructions 33.20% retired LD/ST ( 2.34M) 228 | 229 | lodash.escape 509.40 µs/iter 506.67 µs █ 230 | (463.13 µs … 1.17 ms) 936.88 µs █▃ 231 | (745.70 kb … 825.25 kb) 760.75 kb ▄██▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 232 | 4.19 ipc ( 3.10% stalls) 97.69% L1 data cache 233 | 1.71M cycles 7.18M instructions 33.44% retired LD/ST ( 2.40M) 234 | 235 | escape-goat 668.79 µs/iter 652.33 µs █▄ 236 | (560.54 µs … 1.91 ms) 1.34 ms ██ 237 | ( 1.82 mb … 1.97 mb) 1.97 mb ▅██▇▂▁▁▁▁▂▂▁▁▁▁▁▁▁▁▁▁ 238 | 3.31 ipc ( 4.73% stalls) 95.04% L1 data cache 239 | 2.25M cycles 7.44M instructions 29.08% retired LD/ST ( 2.16M) 240 | 241 | • about.gitlab.com 242 | ------------------------------------------- ------------------------------- 243 | fast-escape-html 755.94 µs/iter 755.79 µs ▄█ 244 | (693.46 µs … 1.21 ms) 1.12 ms ▄██▆ 245 | (917.91 kb … 917.98 kb) 917.93 kb ████▇▄▂▁▁▁▁▁▁▁▁▁▂▂▂▂▂ 246 | 4.79 ipc ( 0.79% stalls) 99.19% L1 data cache 247 | 2.57M cycles 12.31M instructions 19.35% retired LD/ST ( 2.38M) 248 | 249 | escape-html 766.91 µs/iter 768.13 µs ██▃ 250 | (701.46 µs … 1.20 ms) 1.14 ms ▃███ 251 | (917.91 kb … 917.97 kb) 917.93 kb ████▇▃▂▂▁▁▁▁▁▁▁▁▂▂▂▂▁ 252 | 4.74 ipc ( 0.85% stalls) 99.05% L1 data cache 253 | 2.60M cycles 12.32M instructions 19.24% retired LD/ST ( 2.37M) 254 | 255 | @napi-rs/escape 879.66 µs/iter 896.38 µs ▆█ 256 | (789.88 µs … 1.68 ms) 1.16 ms ▂███▅ 257 | (732.54 kb … 732.54 kb) 732.54 kb ▃▅██████▆▄▃▁▂▁▁▁▁▁▁▁▁ 258 | 4.99 ipc ( 22.73% stalls) 79.87% L1 data cache 259 | 2.95M cycles 14.73M instructions 23.03% retired LD/ST ( 3.39M) 260 | 261 | html-escaper 948.21 µs/iter 947.33 µs ▅█▂ 262 | (838.25 µs … 1.61 ms) 1.48 ms ███ 263 | ( 1.36 mb … 1.42 mb) 1.37 mb ▄████▃▂▂▂▂▂▂▁▁▂▁▁▁▁▂▁ 264 | 4.08 ipc ( 3.43% stalls) 97.48% L1 data cache 265 | 3.19M cycles 13.04M instructions 32.88% retired LD/ST ( 4.29M) 266 | 267 | lodash.escape 951.94 µs/iter 947.83 µs ▃█ 268 | (845.63 µs … 1.76 ms) 1.54 ms ██▅ 269 | ( 1.36 mb … 1.42 mb) 1.37 mb ▄███▅▃▁▁▁▁▁▂▂▂▂▂▁▁▁▁▁ 270 | 4.11 ipc ( 3.39% stalls) 97.54% L1 data cache 271 | 3.21M cycles 13.20M instructions 33.12% retired LD/ST ( 4.37M) 272 | 273 | escape-goat 1.18 ms/iter 1.16 ms █ 274 | (994.13 µs … 1.92 ms) 1.75 ms ▂██▃ 275 | ( 3.65 mb … 3.65 mb) 3.65 mb ▂████▅▃▁▁▁▁▂▂▃▃▂▂▃▂▂▂ 276 | 3.41 ipc ( 4.86% stalls) 95.36% L1 data cache 277 | 3.97M cycles 13.52M instructions 29.33% retired LD/ST ( 3.96M) 278 | ``` 279 | 280 | ### Unescape 281 | 282 | ``` 283 | • skk.moe 284 | ------------------------------------------- ------------------------------- 285 | fast-escape-html 625.54 µs/iter 621.04 µs █▄ 286 | (564.67 µs … 1.83 ms) 1.58 ms ██ 287 | (625.88 kb … 691.60 kb) 647.84 kb ██▆▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 288 | 4.95 ipc ( 2.02% stalls) 98.48% L1 data cache 289 | 2.10M cycles 10.41M instructions 31.19% retired LD/ST ( 3.25M) 290 | 291 | html-escaper 635.10 µs/iter 627.46 µs █▇ 292 | (567.17 µs … 1.86 ms) 1.60 ms ██ 293 | (625.88 kb … 691.60 kb) 647.93 kb ██▇▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 294 | 4.98 ipc ( 2.00% stalls) 98.47% L1 data cache 295 | 2.12M cycles 10.55M instructions 30.81% retired LD/ST ( 3.25M) 296 | 297 | lodash.unescape 629.00 µs/iter 623.50 µs █▂ 298 | (570.08 µs … 1.73 ms) 1.62 ms ██ 299 | (625.88 kb … 669.27 kb) 647.90 kb ██▅▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 300 | 5.01 ipc ( 2.00% stalls) 98.49% L1 data cache 301 | 2.11M cycles 10.57M instructions 30.98% retired LD/ST ( 3.27M) 302 | 303 | hexo-util 633.44 µs/iter 626.71 µs █▆ 304 | (574.08 µs … 1.73 ms) 1.52 ms ██ 305 | (625.93 kb … 700.67 kb) 647.87 kb ██▆▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 306 | 5.01 ipc ( 1.96% stalls) 98.51% L1 data cache 307 | 2.12M cycles 10.65M instructions 30.77% retired LD/ST ( 3.28M) 308 | 309 | unescape 1.09 ms/iter 1.08 ms █ 310 | (990.75 µs … 2.08 ms) 1.92 ms ██▆ 311 | ( 1.20 mb … 1.29 mb) 1.25 mb ███▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂▁ 312 | 5.36 ipc ( 1.72% stalls) 99.04% L1 data cache 313 | 3.66M cycles 19.63M instructions 31.63% retired LD/ST ( 6.21M) 314 | 315 | • github.com (incognito) 316 | ------------------------------------------- ------------------------------- 317 | fast-escape-html 1.76 ms/iter 1.76 ms █▂ 318 | (1.59 ms … 2.79 ms) 2.71 ms ▄██▃ 319 | ( 1.61 mb … 1.65 mb) 1.61 mb █████▂▁▁▁▁▁▁▁▁▁▁▂▃▂▂▂ 320 | 4.73 ipc ( 1.79% stalls) 98.53% L1 data cache 321 | 5.89M cycles 27.90M instructions 30.56% retired LD/ST ( 8.53M) 322 | 323 | html-escaper 1.79 ms/iter 1.78 ms █ 324 | (1.62 ms … 2.95 ms) 2.85 ms ▄█▄ 325 | ( 1.61 mb … 1.65 mb) 1.61 mb ████▄▂▁▁▁▁▁▁▁▁▁▁▂▂▁▂▂ 326 | 4.76 ipc ( 1.76% stalls) 98.52% L1 data cache 327 | 5.99M cycles 28.47M instructions 30.06% retired LD/ST ( 8.56M) 328 | 329 | lodash.unescape 1.79 ms/iter 1.78 ms █ 330 | (1.61 ms … 3.21 ms) 2.82 ms ███▄ 331 | ( 1.61 mb … 1.64 mb) 1.61 mb ████▅▂▂▁▁▁▂▁▁▂▂▂▂▂▁▁▁ 332 | 4.79 ipc ( 1.77% stalls) 98.53% L1 data cache 333 | 5.91M cycles 28.34M instructions 30.28% retired LD/ST ( 8.58M) 334 | 335 | hexo-util 1.78 ms/iter 1.77 ms █ 336 | (1.62 ms … 3.00 ms) 2.76 ms ▅██▃ 337 | ( 1.61 mb … 1.62 mb) 1.61 mb ████▄▃▁▁▁▁▁▁▁▁▁▁▁▂▂▂▁ 338 | 4.79 ipc ( 1.75% stalls) 98.54% L1 data cache 339 | 5.95M cycles 28.52M instructions 30.10% retired LD/ST ( 8.59M) 340 | 341 | unescape 3.15 ms/iter 3.07 ms █ 342 | (2.82 ms … 4.70 ms) 4.58 ms ▃██ 343 | ( 3.15 mb … 3.21 mb) 3.16 mb ████▂▂▁▁▁▁▁▁▁▁▃▁▃▃▃▂▂ 344 | 5.25 ipc ( 1.58% stalls) 99.12% L1 data cache 345 | 10.38M cycles 54.51M instructions 30.98% retired LD/ST ( 16.89M) 346 | 347 | • stackoverflow.com (incognito) 348 | ------------------------------------------- ------------------------------- 349 | fast-escape-html 1.71 ms/iter 1.71 ms ▃█ 350 | (1.55 ms … 3.20 ms) 2.74 ms ██▆▃ 351 | ( 1.48 mb … 1.51 mb) 1.50 mb ████▅▁▁▁▁▁▁▁▁▁▁▂▂▂▁▂▂ 352 | 4.65 ipc ( 1.74% stalls) 98.57% L1 data cache 353 | 5.68M cycles 26.44M instructions 30.91% retired LD/ST ( 8.17M) 354 | 355 | html-escaper 1.71 ms/iter 1.70 ms █▇ 356 | (1.58 ms … 2.91 ms) 2.64 ms ██▇ 357 | ( 1.48 mb … 1.52 mb) 1.50 mb ████▄▂▁▁▁▁▁▁▁▁▁▂▁▂▂▂▂ 358 | 4.70 ipc ( 1.74% stalls) 98.57% L1 data cache 359 | 5.73M cycles 26.94M instructions 30.46% retired LD/ST ( 8.21M) 360 | 361 | lodash.unescape 1.70 ms/iter 1.69 ms █▂ 362 | (1.57 ms … 3.00 ms) 2.70 ms ██▄ 363 | ( 1.48 mb … 1.51 mb) 1.50 mb ███▇▃▂▁▁▁▁▁▁▁▁▁▁▂▂▂▁▁ 364 | 4.72 ipc ( 1.74% stalls) 98.57% L1 data cache 365 | 5.69M cycles 26.87M instructions 30.68% retired LD/ST ( 8.25M) 366 | 367 | hexo-util 1.76 ms/iter 1.74 ms █▅ 368 | (1.59 ms … 4.05 ms) 2.98 ms ██▅ 369 | ( 1.48 mb … 1.51 mb) 1.50 mb ███▅▃▂▁▁▁▁▁▂▂▂▂▂▁▁▁▁▁ 370 | 4.67 ipc ( 1.69% stalls) 98.58% L1 data cache 371 | 5.80M cycles 27.07M instructions 30.47% retired LD/ST ( 8.25M) 372 | 373 | unescape 2.86 ms/iter 2.89 ms ▆█ ▂ 374 | (2.67 ms … 3.87 ms) 3.60 ms ██▆█▆▂ 375 | ( 2.97 mb … 2.98 mb) 2.97 mb ██████▄▃▂▂▁▁▃▃▃▁▄▂▂▁▁ 376 | 5.25 ipc ( 1.60% stalls) 99.13% L1 data cache 377 | 9.56M cycles 50.22M instructions 31.35% retired LD/ST ( 15.75M) 378 | 379 | • www.google.com (incognito) 380 | ------------------------------------------- ------------------------------- 381 | fast-escape-html 775.50 µs/iter 767.25 µs █▃ 382 | (699.54 µs … 2.03 ms) 1.81 ms ██ 383 | (829.08 kb … 954.86 kb) 953.04 kb ██▇▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 384 | 4.80 ipc ( 2.21% stalls) 98.16% L1 data cache 385 | 2.60M cycles 12.48M instructions 29.47% retired LD/ST ( 3.68M) 386 | 387 | html-escaper 794.20 µs/iter 795.04 µs ▄█ 388 | (705.92 µs … 1.90 ms) 1.84 ms ██▃ 389 | (829.08 kb … 954.80 kb) 952.84 kb ███▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 390 | 4.77 ipc ( 2.15% stalls) 98.18% L1 data cache 391 | 2.66M cycles 12.67M instructions 29.13% retired LD/ST ( 3.69M) 392 | 393 | lodash.unescape 786.99 µs/iter 783.17 µs █▆ 394 | (704.96 µs … 1.98 ms) 1.84 ms ██▂ 395 | (857.23 kb … 954.80 kb) 953.26 kb ███▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 396 | 4.80 ipc ( 2.18% stalls) 98.17% L1 data cache 397 | 2.64M cycles 12.68M instructions 29.28% retired LD/ST ( 3.71M) 398 | 399 | hexo-util 797.88 µs/iter 789.46 µs █ 400 | (715.83 µs … 2.07 ms) 1.89 ms ██ 401 | (829.12 kb … 954.86 kb) 953.08 kb ██▇▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 402 | 4.78 ipc ( 2.15% stalls) 98.16% L1 data cache 403 | 2.67M cycles 12.76M instructions 29.10% retired LD/ST ( 3.71M) 404 | 405 | unescape 1.43 ms/iter 1.43 ms █▄ 406 | (1.29 ms … 2.48 ms) 2.41 ms ██▇ 407 | ( 1.46 mb … 1.54 mb) 1.51 mb ████▄▁▁▁▁▁▁▁▁▂▁▁▁▁▂▁▂ 408 | 5.29 ipc ( 1.71% stalls) 98.97% L1 data cache 409 | 4.76M cycles 25.18M instructions 29.94% retired LD/ST ( 7.54M) 410 | 411 | • about.gitlab.com 412 | ------------------------------------------- ------------------------------- 413 | fast-escape-html 1.41 ms/iter 1.41 ms ▃█ 414 | (1.28 ms … 2.32 ms) 2.29 ms ██▆▂ 415 | ( 1.54 mb … 1.57 mb) 1.55 mb ████▅▂▁▁▁▁▁▁▁▁▁▂▂▂▂▁▂ 416 | 4.80 ipc ( 2.38% stalls) 98.10% L1 data cache 417 | 4.72M cycles 22.67M instructions 29.38% retired LD/ST ( 6.66M) 418 | 419 | html-escaper 1.44 ms/iter 1.43 ms ▂█ 420 | (1.29 ms … 2.88 ms) 2.44 ms ██▇ 421 | ( 1.54 mb … 1.57 mb) 1.55 mb ████▄▂▁▁▁▁▁▁▁▁▁▂▁▂▂▂▁ 422 | 4.81 ipc ( 2.35% stalls) 98.08% L1 data cache 423 | 4.82M cycles 23.17M instructions 28.97% retired LD/ST ( 6.71M) 424 | 425 | lodash.unescape 1.42 ms/iter 1.41 ms ▂█ 426 | (1.29 ms … 2.44 ms) 2.33 ms ██▅ 427 | ( 1.54 mb … 1.57 mb) 1.55 mb ████▃▂▁▁▁▁▁▁▁▁▁▁▂▂▂▂▁ 428 | 4.83 ipc ( 2.37% stalls) 98.10% L1 data cache 429 | 4.77M cycles 23.06M instructions 29.16% retired LD/ST ( 6.72M) 430 | 431 | hexo-util 1.43 ms/iter 1.42 ms █ 432 | (1.30 ms … 2.59 ms) 2.36 ms ██▃ 433 | ( 1.54 mb … 1.58 mb) 1.55 mb ███▇▂▂▁▁▁▁▁▁▁▁▁▁▂▁▂▁▁ 434 | 4.83 ipc ( 2.34% stalls) 98.12% L1 data cache 435 | 4.80M cycles 23.20M instructions 28.99% retired LD/ST ( 6.72M) 436 | 437 | unescape 2.64 ms/iter 2.61 ms █▃ 438 | (2.38 ms … 4.12 ms) 3.86 ms ▆██▄ 439 | ( 2.73 mb … 2.77 mb) 2.74 mb ████▅▃▁▁▁▁▁▁▁▁▁▂▁▄▂▃▂ 440 | 5.26 ipc ( 1.79% stalls) 98.91% L1 data cache 441 | 8.79M cycles 46.25M instructions 29.91% retired LD/ST ( 13.83M) 442 | ``` 443 | 444 | ## License 445 | 446 | [MIT](./LICENSE). 447 | 448 | ---- 449 | 450 | **fast-escape-html** © [Sukka](https://github.com/SukkaW), Authored and maintained by Sukka with help from contributors ([list](https://github.com/SukkaW/fast-escape-html/graphs/contributors)). 451 | 452 | > [Personal Website](https://skk.moe) · [Blog](https://blog.skk.moe) · GitHub [@SukkaW](https://github.com/SukkaW) · Telegram Channel [@SukkaChannel](https://t.me/SukkaChannel) · Twitter [@isukkaw](https://twitter.com/isukkaw) · Keybase [@sukka](https://keybase.io/sukka) 453 | 454 |

455 | 456 | 457 | 458 |

459 | -------------------------------------------------------------------------------- /benchmark/fixtures/skk.moe.html: -------------------------------------------------------------------------------- 1 | Sukka (@sukkaw)

Home

Hello,I'm Sukka /'sʊkɑː/

Front-end Developer / Open Sourceror / Blogger

I am

Core team member of Hexo

Team member of RSSHub

Founder of OpenIPDB

Active contributor of Next.js, htmlnano, and many more.

About Me

I am a self-taught coder who is passionate about open source. I have created 800+ Pull Requests toward 100+ repositories, gained about 2k followers, and earned more than 5k stars on GitHub.

Besides contributing to the open source community, I am also a minimalist obsessed with the idea of speed and lightweight.

Outside of coding, I am a fan of railway, aviation, and mechanical engineering. I also enjoy watching documentaries, animes, and movies, playing sandbox / tower-defense video games, and reading sci-fi novels by Jules Verne.

2 | 3 | --------------------------------------------------------------------------------