npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@speed-highlight/core

v2.1.0

Published

A tiny, fast, simple syntax highlighter for the web and the terminal in JavaScript

Downloads

66,703,921

Readme

speed-highlight

NPM Version NPM Downloads

A tiny, fast, simple syntax highlighter for the web and the terminal in JavaScript

  • Tiny (~1.5 kB gzipped core, ~1 kB gzipped per language)
  • Fast (generally outperforms Prism and highlight.js, see the benchmark)
  • Simple (zero dependencies)

Playground

Screenshot

Quick start

npm i @speed-highlight/core

In a terminal, print the highlighted string:

import { highlightANSI } from '@speed-highlight/core';
import theme from '@speed-highlight/core/themes/default.js';

console.log(await highlightANSI('console.log("hello")', 'js', theme));

In a component, highlight a string and render it:

import { useEffect, useState } from 'react';
import { highlightHTML } from '@speed-highlight/core';
import '@speed-highlight/core/themes/default.css';

export function Code({ code, lang }) {
	const [html, setHtml] = useState('');

	useEffect(() => {
		// highlightHTML is async (languages load on first use), skip stale results
		let stale = false;
		highlightHTML(code, lang).then(result => { if (!stale) setHtml(result); });
		return () => { stale = true; };
	}, [code, lang]);

	return <div className={`shj-lang-${lang} shj-block`} dangerouslySetInnerHTML={{ __html: html }} />;
}

How it works

The tokenizer runs a language's regex rules over your code and emits typed tokens (kwd, str, cmnt, ...). On the web each token becomes a <span class="shj-syn-kwd">; in the terminal it becomes an ANSI escape code. A theme is just CSS (or a token-to-escape map) coloring those names, which is why themes are under 1 kB and writing your own is a few lines.

Comparison

Highlighters trade size for grammar fidelity: TextMate engines (Shiki, starry-night) are the most faithful and heaviest, mature regex engines (highlight.js, Prism) sit in the middle, lightweight regex tokenizers (speed-highlight, sugar-high) are the smallest and approximate on exotic syntax.

| | Core (gzip) | Per language (gzip) | Languages | Grammar model | Terminal | Status | Choose it for | | --- | --- | --- | --- | --- | --- | --- | --- | | speed-highlight | 1.5 kB | 0.07–1.6 kB | ~30 | lightweight regex | ✅ built in | ✅ v2 (this repo) | runtime highlighting where size and startup matter | | sugar-high | 1.7 kB | 0.18–2.9 kB | 25 | lightweight regex | ❌ | ✅ v2.0.0 | CSS-variable theming, JSX/TSX-aware JavaScript | | Prism | 3.1 kB | 0.3–3 kB | ~290 | mature regex | ❌ | ⚠️ v1.30.0, frozen since Mar 2025 (v2 rewrite) | its plugin ecosystem | | highlight.js | 8.3 kB | 0.3–2.5 kB | ~190 | mature regex | ❌ (via wrappers) | ✅ v11.11 (Jun 2026) | broad auto-detection and rare languages | | Shiki | 35 kB + engine: 145 kB WASM or 20 kB JS | 5–16 kB | ~220 | TextMate (VS Code) | ✅ via @shikijs/cli | ✅ v4.4, very active | highest fidelity (the same grammars as VS Code); zero client JS when run at build time | | starry-night | 185 kB incl. WASM | 3–25 kB | 600+ | TextMate (GitHub) | ❌ | ✅ active | GitHub-identical rendering in Node |

Sizes are min+gzip, measured from the installed packages at the versions shown (per-language = range over the benchmark corpus; starry-night figures from its own README). Wrappers reuse these engines and inherit their numbers: lowlight/refractor wrap highlight.js/Prism for virtual DOMs, rehype-pretty-code and bright wrap Shiki, and the terminal-only emphasize and cli-highlight wrap highlight.js grammars into ANSI. Editors (CodeMirror, Monaco, tree-sitter) are a different category.

If you highlight at build time and bytes do not matter, use Shiki. speed-highlight's case is the opposite one: highlighting at runtime, where the entire library with all 34 grammars bundled into one file gzips to 9.0 kB, barely more than highlight.js's core alone, before it has loaded a single grammar.

Web usage

In a component

Frameworks own their DOM, so highlight the string and render it, as in the quick start (mutating a mounted node with highlightElement gets wiped on the next render). The output is HTML-escaped (&, <, >), safe to inject even for untrusted code; the shj-lang-* and shj-block classes hook it into the theme. The same pattern works in Vue, Svelte, and Angular; ready-made components are in #85.

On a plain page

Mark code blocks with a shj-lang-* class and call highlightAll once:

<div class="shj-lang-js">console.log('hello')</div>
<code class="shj-lang-js">inline code</code>

<script type="module">
	import { highlightAll } from '@speed-highlight/core';
	highlightAll();
</script>

Blocks are a single <div> instead of <pre><code> so the line-number gutter can be laid out inside; the shj-lang- prefix avoids colliding with Prism's language-* during a migration.

For per-element control use highlightElement. It renders a code element inline and anything else as a block, accepts block as an override, and sets data-lang so a theme can render a language header with content: attr(data-lang):

import { highlightElement } from '@speed-highlight/core';

await highlightElement(element, 'js', { showLineNumbers: true });

Detect the language

Detection is a separate ~1 kB import so the core stays small. It recognizes about 20 common languages and returns 'plain' when unsure:

import { highlightElement } from '@speed-highlight/core';
import { detectLanguage } from '@speed-highlight/core/detect';

element.textContent = code;
await highlightElement(element, detectLanguage(code));

Control loading and bundling

Languages load lazily through a loader. Replace it with setLoader to add custom languages or restrict what your bundler includes; a name the loader cannot resolve renders as plain text:

import { setLoader, defaultLoader } from '@speed-highlight/core';

// add custom languages on top of the bundled ones
setLoader(name => customs[name] ?? defaultLoader(name));

// or allow only the languages your bundler can code-split
setLoader(name => ({
	js: () => import('@speed-highlight/core/languages/js.js'),
	css: () => import('@speed-highlight/core/languages/css.js'),
})[name]?.());

For full tree-shaking skip the loader entirely: tokenizeWith takes every language from the caller, so a bundler keeps only what you import. Include the sub-languages a grammar embeds (html uses css and js; js uses jsdoc, todo, and regex). A sub that is not given keeps the type of its rule and only skips the inner highlighting:

import { tokenizeWith } from '@speed-highlight/core/tokenize';
import { html, css, js, jsdoc, todo, regex } from '@speed-highlight/core/languages';

tokenizeWith(code, html, (str, type) => { /* ... */ }, { languages: { css, js, jsdoc, todo, regex } });

[!NOTE] highlightHTML and tokenizeWith never touch the DOM, so they also run server-side or in a web worker: highlight there and send the string over.

CDN (no build step)

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@speed-highlight/core@2/dist/themes/default.css">
<script type="module">
	import { highlightAll } from 'https://cdn.jsdelivr.net/npm/@speed-highlight/core@2/dist/index.js';
	highlightAll();
</script>

Terminal usage

highlightANSI returns a string ready to print; the theme is required, import one from themes/*.js:

import { highlightANSI } from '@speed-highlight/core';
import theme from '@speed-highlight/core/themes/atom-dark.js';

console.log(await highlightANSI(code, 'js', theme));

A terminal theme is a plain token-to-escape map, built with the termcolor helpers or raw escapes:

import * as col from '@speed-highlight/core/themes/termcolor.js';

export default {
	kwd: col.red,
	str: col.green,
	cmnt: col.gray,
};

For Deno, use the deno module:

import { highlightANSI } from 'https://deno.land/x/speed_highlight_js/dist/index.js';
import theme from 'https://deno.land/x/speed_highlight_js/dist/themes/default.js';

console.log(await highlightANSI('console.log("hello")', 'js', theme));

API

The main entry covers most apps; reach for /tokenize when you want the raw token stream and full control over what gets bundled. Everything ships TypeScript types.

| Entry | Export | Description | | --- | --- | --- | | @speed-highlight/core | highlightAll(opt?) | Highlight every element with a shj-lang-* class | | | highlightElement(elm, lang?, opt?) | Highlight one element (language read from its class by default) | | | highlightHTML(src, lang, opt?) | Highlight a string, resolves to an HTML string | | | highlightANSI(src, lang, theme) | Highlight a string, resolves to an ANSI string for terminals | | | tokenize(src, lang, onToken) | Loader-based tokenizer, calls onToken(text, type) | | | setLoader(loader) / defaultLoader | Replace or compose how language names are resolved | | .../detect | detectLanguage(code) | Guess the language, 'plain' when unsure | | .../tokenize | tokenizeWith(src, lang, onToken, opt?), tokenizer | Registry-free synchronous tokenizer (and the underlying generator), languages passed by the caller | | .../languages | one named export per language | Grammars, import only what you need | | .../themes/*.css | | Web themes | | .../themes/*.js | | Terminal themes, plus termcolor.js helpers |

lang is a name ('js') or a grammar object passed directly. opt is { block?: boolean, showLineNumbers?: boolean }: line numbers are opt-in, block defaults to true, except that highlightElement and highlightAll read it off the element instead, where a code element is inline and anything else is a block.

Languages

| Name | CSS Class | Support | Detection | Size (gzip, 14.4 kB total) | | --- | --- | --- | --- | --- | | Assembly | shj-lang-asm | | ✅ | 194 B | | Bash | shj-lang-bash | | ✅ | 430 B | | Brainfuck | shj-lang-bf | increment, operator, print, comment | ❌ | 137 B | | C | shj-lang-c | | ✅ | 429 B | | CSS | shj-lang-css | comment, str, selector, units, function, ... | ✅ | 332 B | | CSV | shj-lang-csv | punctuation, ... | ❌ | 96 B | | Diff | shj-lang-diff | | ✅ | 144 B | | Dockerfile | shj-lang-docker | | ✅ | 566 B | | Git | shj-lang-git | comment, insert, deleted, string, ... | ❌ | 222 B | | Go | shj-lang-go | | ✅ | 329 B | | HTML | shj-lang-html | inline style and event handlers | ✅ | 726 B | | HTTP | shj-lang-http | keywork, string, punctuation, variable, version | ✅ | 986 B | | INI | shj-lang-ini | | ❌ | 158 B | | Java | shj-lang-java | | ✅ | 457 B | | JavaScript | shj-lang-js | basic syntax, regex, jsdoc, json, template literals | ⛔ reported as TypeScript | 820 B | | JSDoc | shj-lang-jsdoc | | ❌ | 251 B | | JSON | shj-lang-json | string, number, bool, ... | ❌ | 172 B | | LeanPub Markdown | shj-lang-leanpub-md | | ❌ | 1.2 kB | | Log | shj-lang-log | number, string, comment, errors | ❌ | 223 B | | Lua | shj-lang-lua | | ✅ | 273 B | | Makefile | shj-lang-make | | ✅ | 223 B | | Markdown | shj-lang-md | | ✅ | 1.1 kB | | Perl | shj-lang-pl | | ✅ | 329 B | | Plain text | shj-lang-plain | | ❌ | 71 B | | Python | shj-lang-py | | ✅ | 416 B | | Regex | shj-lang-regex | count, set, ... | ❌ | 176 B | | Rust | shj-lang-rs | | ✅ | 414 B | | SQL | shj-lang-sql | number, string, function, ... | ✅ | 1.7 kB | | TODO | shj-lang-todo | | ❌ | 185 B | | TOML | shj-lang-toml | comment, table, string, bool, variable | ❌ | 236 B | | TypeScript | shj-lang-ts | js syntax, ts keyword, types | ✅ | 911 B | | URI | shj-lang-uri | | ✅ | 176 B | | XML | shj-lang-xml | | ✅ | 511 B | | YAML | shj-lang-yaml | comment, numbers, variable, string, bool | ✅ | 208 B |

Themes

| Name | Terminal (gzip) | Web (gzip) | | --- | --- | --- | | default | ✅ 174 B | ✅ 603 B | | atom-dark | ✅ 174 B | ✅ 699 B | | dark | ❌ | ✅ 695 B | | github-dark | ❌ | ✅ 691 B | | github-dim | ❌ | ✅ 700 B | | github-light | ❌ | ✅ 672 B | | visual-studio-dark | ❌ | ✅ 694 B |

Custom languages

A language is an array of rules. Every rule's regex (global flag required) is tried; the earliest match in the string wins, ties go to the earlier rule:

export default [
	{ match: /\/\/.*/g, type: 'cmnt' },
	{ expand: 'str' },
	{ expand: 'num' },
	{ match: /\b(if|else|for|while|return)\b/g, type: 'kwd' },
];
  • { match, type } tags what the regex matches with a token type
  • { expand } reuses a shared pattern: 'num', 'str', or 'strDouble'
  • { match, sub } re-tokenizes the matched region with another language: a name (loaded through the loader), an inline grammar array, or a function code => name | grammar deciding per match

A language can also set a default token for unmatched text by exporting { type, sub } instead of a bare array (see http.js). Use a grammar by passing it directly as lang, or register a name with setLoader. To extend an existing language, spread it after your rules:

import js from '@speed-highlight/core/languages/js.js';

export default [
	{ match: /\b(signal|effect)\b/g, type: 'func' },
	...js,
];

Token types:

| Token | Used for | | Token | Used for | | Token | Used for | | --- | --- | --- | --- | --- | --- | --- | --- | | kwd | keywords | | type | types | | esc | escape sequences | | str | strings | | class | classes | | section | section delimiters | | num | numbers | | var | variables | | insert | inserted parts (diff) | | cmnt | comments | | oper | operators | | deleted | deleted parts (diff) | | func | functions | | bool | booleans | | err | errors |

Missing a language? Open an issue or send a PR adding a file to src/languages/.

Custom themes

A web theme colors the token classes; start from default.css and override:

[class*="shj-lang-"] { color: #f8f8f2; background: #282a36; }
.shj-syn-kwd { color: #ff79c6; }
.shj-syn-str, .shj-syn-insert { color: #50fa7b; }
.shj-syn-cmnt { color: #6272a4; font-style: italic; }
.shj-numbers { color: #6272a4; }

Display-mode hooks: .shj-inline (inside code), .shj-block, and .shj-numbers for the gutter. Terminal themes are the token-to-escape maps shown in Terminal usage.

Migrating from v1

| v1 | v2 | | --- | --- | | highlightText(src, lang) | highlightHTML(src, lang) | | printHighlight(src, lang) from /terminal | console.log(await highlightANSI(src, lang, theme)) | | setTheme('atom-dark') | pass the theme: highlightANSI(src, lang, theme) | | loadLanguage(name, grammar) | setLoader(...) or pass the grammar directly as lang | | @speed-highlight/core/terminal entry | merged into @speed-highlight/core | | common.js shared patterns | { expand: 'num' \| 'str' \| 'strDouble' } built into the tokenizer | | { hideLineNumbers: true } | now the default, line numbers are opt-in with { showLineNumbers: true } | | oneline display mode | removed, a div is always a block | | highlightElement(elm, lang, mode, opt) | the mode moved into the options: highlightElement(elm, lang, { block }) | | shj-multiline class | shj-block |

Benchmark

$ npm run benchmark
node v26.7.0, darwin arm64, Apple M4
corpus: js, css, json, md, sql, py, bash, tiled to 3 sizes (tiny (1 KB) / medium (16 KB) / huge (128 KB)), median of 9 trials per language, averaged across the corpus

                              tiny (1 KB)     medium (16 KB)      huge (128 KB)
speed-highlight         1,144,010 ops/min     74,720 ops/min      8,583 ops/min
prismjs                   655,152 ops/min     34,310 ops/min      2,575 ops/min
highlight.js              595,113 ops/min     48,666 ops/min      5,473 ops/min
sugar-high                238,406 ops/min     13,405 ops/min      1,376 ops/min
shiki (js engine)          89,925 ops/min      6,682 ops/min        841 ops/min

cold start (import + first highlight of test.js):
speed-highlight                    7.3 ms
prismjs                            8.6 ms
highlight.js                        19 ms
sugar-high                          18 ms
shiki (js engine)                  169 ms

speed-highlight per language (warm, median of 9 trials):
                              tiny (1 KB)     medium (16 KB)      huge (128 KB)
js                        666,642 ops/min     40,773 ops/min      4,704 ops/min
css                     1,006,895 ops/min     62,774 ops/min      7,284 ops/min
json                    1,964,840 ops/min    127,890 ops/min     15,057 ops/min
md                      1,080,739 ops/min     86,883 ops/min     10,453 ops/min
sql                     1,369,247 ops/min     88,513 ops/min     10,180 ops/min
py                        974,179 ops/min     56,165 ops/min      6,260 ops/min
bash                      945,526 ops/min     60,045 ops/min      6,141 ops/min

Identical inputs from examples/languages/, tiled up to each size bucket, HTML-string output for every library, one op = one highlighted file. Each figure is the median of 9 repeated trials, reported as ops/min (the huge bucket can drop below 1 op/sec for the slower libraries). Warm runs have grammars preloaded; cold start is import plus first highlight, measured once (not size-swept). Shiki does more work by design (see Comparison).