Sanitize HTML

GreenNode + BrowserText
napi1.35–3.6× faster

HTML sanitization via Mozilla's ammonia engine.

Version
0.2.0

Install

pnpm add @amigo-labs/sanitize-html

Benchmarks

Trend (17 pts)

Benchmark

transform ol→ul (simple rename) - small (~80B)

1.63× vs slowest
  • sanitize-html (npm)48.92K hz·1.63×
  • amigo (baseline, no transform)32.63K hz·1.09×
  • regex-wrapper + amigo30.76K hz
  • tokenizer-wrapper + amigo30.00K hz

Benchmark

transform ol→ul - medium (~6KB, 100 lists)

3.19× vs slowest
  • amigo (baseline, no transform)7.37K hz·3.19×
  • regex-wrapper + amigo5.81K hz·2.51×
  • tokenizer-wrapper + amigo3.66K hz·1.59×
  • sanitize-html (npm)2.31K hz

Benchmark

transform ol→ul - heavy (~40KB, 1000 transforms)

3.67× vs slowest
  • amigo (baseline, no transform)989 hz·3.67×
  • regex-wrapper + amigo750 hz·2.78×
  • sanitize-html (npm)355 hz·1.32×
  • tokenizer-wrapper + amigo269 hz

Benchmark

transform rewrite ol→ul + add class (attribute mutation) - 1000 tags

  • tokenizer-wrapper + amigo260 hz
  • sanitize-html (npm)258 hz

Benchmark

sanitize - small safe HTML (~200 chars)

12.53× vs slowest
  • @amigo-labs/sanitize-htmlnapi54.58K hz·12.53×
  • sanitize-html (npm)40.29K hz·9.25×
  • isomorphic-dompurify4.36K hz

Benchmark

sanitize - medium with XSS (~2 KB)

34.09× vs slowest
  • @amigo-labs/sanitize-htmlnapi12.41K hz·34.09×
  • sanitize-html (npm)4.81K hz·13.20×
  • isomorphic-dompurify364 hz

Benchmark

sanitize - large document (~100 KB)

30.60× vs slowest
  • @amigo-labs/sanitize-htmlnapi461 hz·30.60×
  • sanitize-html (npm)127 hz·8.46×
  • isomorphic-dompurify15.1 hz

Not enough history to draw a trend.

17 commits · last 2026-08-18

README

@amigo-labs/sanitize-html

npm version npm downloads license

Blazing fast HTML sanitization powered by Rust via NAPI-RS. A stream-based native Node.js sanitizer built directly on top of the html5ever tokenizer — faster than the JS sanitize-html / isomorphic-dompurify packages on realistic inputs. Live numbers are on the dashboard.

Installation

npm install @amigo-labs/sanitize-html

Usage

import { sanitize, isClean } from "@amigo-labs/sanitize-html";

// Sanitize HTML (removes dangerous tags/attributes)
const clean = sanitize('<p>Hello <script>alert("xss")</script></p>');
// "<p>Hello </p>"

// Check if HTML is already clean
const safe = isClean("<p>Hello</p>"); // true
const unsafe = isClean('<img onerror="alert(1)">'); // false

// With custom options
const result = sanitize("<div class='foo'>bar</div>", {
  allowedTags: ["div", "p", "b", "i"],
  allowedAttributes: { div: ["class"] },
});

API

sanitize(html, options?): string

Sanitizes HTML by removing dangerous tags and attributes. Returns the cleaned HTML string.

isClean(html, options?): boolean

Returns true if the HTML contains no dangerous content (i.e. sanitization would not change it).

Options

OptionTypeDescription
allowedTagsstring[]Tags to allow (default: a conservative safe set — see DEFAULT_TAGS in src/v2.rs)
allowedAttributesRecord<string, string[]>Attributes to allow per tag
allowedClassesRecord<string, string[]>CSS classes to allow per tag
allowedSchemesstring[]URL schemes accepted in href/src/… (default: http, https, mailto, ftp, tel, …)
stripCommentsbooleanWhether to strip HTML comments (default: true)
linkRelstringValue for rel attribute on <a href> (default: "noopener noreferrer")

Performance

Live benchmark numbers vs upstream sanitize-html and isomorphic-dompurify are tracked on the dashboard and in docs/data.json. The margin grows with input size — the streaming tokenizer avoids the DOM build cost the JS alternatives pay.

Install for the browser

The same import works in Angular, React, Vite, esbuild, and webpack ≥ 5 — the bundler picks the WASM build via the browser conditional export:

import { sanitize } from '@amigo-labs/sanitize-html'

ammonia + html5ever is ~250–400 KB gzipped (the heaviest text-category crate, but still under the 500 KB budget). The napi Either<String, f64> input quirk is dropped on the browser side — pass a string (or null/undefined) directly.

Supported Platforms

PlatformArchitecture
Linuxx64 (glibc), x64 (musl), arm64
macOSx64, arm64
Windowsx64

License

MIT

Perf review

Perf-Review: @amigo-labs/sanitize-html

Status: 🟢 Green · Reviewed: 2026-04-21 · Version: 0.2.0

Verdict

1.63×–4.15× vs. sanitize-html npm on the sanitize surface; 39.6×–122× vs. isomorphic-dompurify on large documents. HTML parsing + rule dispatch + serialization is exactly the Green shape from commonmark/turndown: Buffer in/String out, substantial compute, no chain API. Mozilla’s ammonia is the backend (also used in servo and the Mozilla Observatory scanners); on 100 KB input with XSS content we are 4.15× faster than upstream sanitize-html. The transform API (regex + tokenizer-based wrappers) gives medium/heavy HTML mutation an additional 1.5–2.6× over the npm equivalent.

Classification rationale

  1. ammonia = html5ever + whitelist rules. The parser (html5ever, Mozilla Servo) is SIMD-accelerated and zero-GC. Rule dispatch in Rust is a pattern match on node type (no JS hashmap lookup per tag).
  2. V0.2 upgrade with the hybrid engine. Tokenizer wrapper + regex wrapper coexist — regex is the fastest path for simple tag renames (ol→ul), the tokenizer for attribute mutations. The user picks the right tool per transform operation.
  3. The small-case delta is conservative. 200-char safe HTML: 1.63× vs. sanitize-html. For very small inputs the FFI floor is more visible — but it stays above 1×, hence Green-compliant.
  4. The DOMPurify comparison is an extreme case. DOMPurify is browser-oriented and massively slow in a Node context (jsdom-based). The 122× win on 100 KB confirms that DOMPurify is not a real server alternative.

Evidence

Measured speedup (docs/data.json, 2026-04-18)

Sanitize surface:

Scenario@amigo-labs/sanitize-htmlsanitize-html npmisomorphic-dompurifyvs. sanitize-htmlvs. dompurify
small safe HTML (~200 chars)45 747 Hz28 032 Hz1 157 Hz1.63×39.5×
medium with XSS (~2 KB)10 320 Hz3 928 Hz227 Hz2.63×45.5×
large document (~100 KB)375.5 Hz90.5 Hz9.5 Hz4.15×39.6×

Transform surface (ol→ul simple rename):

Scenarioregex-wrapper + amigotokenizer-wrapper + amigosanitize-html npmbest-of-amigo vs. upstream
small (~80B)25 923 Hz25 409 Hz35 525 Hz0.73× (small: upstream still wins)
medium (~6KB, 100 lists)4 737 Hz3 032 Hz1 821 Hz2.60× (regex-wrapper)
heavy (~40KB, 1000 transforms)602.3 Hz178.7 Hz283.4 Hz2.13× (regex-wrapper)

Realistic use-case

User-generated-content sanitization — comments, forum posts, rich-text-editor output. Typically 500 B – 10 KB. Email HTML rendering with XSS prevention for incoming mail. Markdown-to-HTML pipeline after @amigo-labs/commonmark. Content import from legacy systems with unsafe HTML. Median: 2 KB of safe or untrusted HTML.

The transform use case is rarer: CMS content migration (e.g. ol→ul for style consistency), rich-text-editor normalization (WYSIWYG output cleanup).

Benchmark gaps

  • Very large (1 MB+) not benchmarked. Realistic for article-corpus processing.
  • Allow-list-heavy configs (many custom allowed tags + attributes) not measured in isolation. Rule-set size affects dispatch overhead.
  • Multi-transform chain (tokenizer + regex in one pass) not benchmarked — users would have to make two calls today.

API surface

Based on sanitize-html parity plus the hybrid-engine additions:

  • sanitize(html, options?) — main call, returns the cleaned HTML string
  • Options: allowedTags, allowedAttributes, allowedClasses, allowedSchemes, transformTags, textFilter, parser, etc. (sanitize-html parity)
  • Hybrid-engine flag or auto-select: ideally the user should not have to decide between regex/tokenizer.

Bundle / binary size

ammonia + html5ever + deps: ~2–3 MB per target. One of the larger binaries in the portfolio, but justified by the 4× speedup + security.

FFI-overhead baseline

  • 100 KB sanitize: buffer input ~180 ns, 80 KB string output ~28 µs UTF conversion. Against ~2.7 ms of Rust parse+sanitize = 1% FFI share. Tolerable.
  • 200-char sanitize: FFI ~1 µs against ~22 µs Rust = ~5%. Still Green territory.

Phase-C optimization checklist

#LeverApplicableNotes
C.1Input-type minimization✅ already doneBuffer zero-copy
C.2Output-type minimization✅ already done
C.3Batch API (sanitizeMany)🟡 potentialBulk UGC import, comments migration. Not measured
C.4Stateful API (pre-compiled rule set)✅ already doneammonia::Builder pattern: the user constructs the config once and calls .clean() many times
C.5Parallelization (rayon over document batches)🟡 potentialOnly for a batch API
C.6Algorithm swap❌ not applicableammonia + html5ever is best-in-class
C.7Allocator tuning✅ already done
C.8Bundle-size⚠️ trade-off~2-3 MB is large, but unavoidable for an HTML parser. LTO enabled

Action plan

Keep as-is. Green across all production scenarios. The v0.2 hybrid engine was the last major sprint.

Maintenance:

  1. Add a very-large bench (1 MB document).
  2. Bench an allow-list-heavy config (e.g. 50+ custom tags).
  3. The transform small case (80 B) is the only non-Green spot — upstream sanitize-html wins there thanks to the JS regex fast path. Not critical, but document it.

References

  • Crate: crates/sanitize-html
  • Bench (main): crates/sanitize-html/__bench__/index.bench.ts
  • Bench (transforms): crates/sanitize-html/__bench__/transforms.bench.ts
  • Lib: crates/sanitize-html/src/lib.rs
  • Cargo: crates/sanitize-html/Cargo.toml
  • docs/packages.json speedup: "1.63–4.1× faster"