Skip to content

Repository files navigation

</​hypertag> npm-version-badge npm-license-badge

The smallest, fastest HTML parser that skips the DOM.

hypertag parses an HTML string and returns the tag attributes you ask for as plain objects: a getElementsByTagName that needs no DOM. Zero dependencies, about 0.8 kB, and in the benchmark below the fastest and smallest way to pull <meta>, <link>, and other tags out of HTML. Runs on Node, Deno, Bun, and the edge.

✨ Features

  • ✅  Tiny. ~0.8 kB bundled, 35x smaller than htmlparser2 and ~1000x smaller than jsdom, so it barely touches an edge bundle.
  • ✅  Zero dependencies. Nothing to audit, break, or bloat your tree.
  • ✅  Just the tags. Name the tags you want, get their attributes back as plain objects. No DOM, no selectors to learn.
  • ✅  Fast and light. 2.6x faster than node-html-parser and up to 95x faster than the DOM parsers, and it builds no tree so it retains almost no memory.
  • ✅ Auditable. One small file you can read in a minute, 100% test coverage. ci-badge

📦 Install

npm install hypertag

💻 Use

import parse from 'hypertag'          // ESM
// const parse = require('hypertag')  // CommonJS

const html = `
  <html><head>
    <meta name="hello" content="world">
    <meta name="hello" content="moon">
  </head><body>
    <div><h1>Hello, world!</h1></div>
  </body></html>
`

const result = parse(html, 'meta')
console.log(result)

[
  {
    '<' : 'meta',
    name: 'hello',
    content: 'world'
  },
  {
    '<' : 'meta',
    name: 'hello',
    content: 'moon'
  }
]

Examples

Getting Favicons

const result = parse(html, 'link')
  .filter(({rel}) => /^(shortcut\s+)?icon/i.test(rel ?? ''))

[
  {
    '<': 'link',
    rel: 'icon',
    href: 'favicon.png',
    sizes: '16x16'
    type: 'image/png'
  }
]

Getting OpenGraph Images

const result = parse(html, 'meta')
  .filter(({property}) => property?.toLowerCase() === 'og:image')

[
  {
    '<': 'meta',
    property: 'og:image',
    content: 'http://static01.nyt.com/images/2015/02/19/arts/international/19iht-btnumbers19A/19iht-btnumbers19A-facebookJumbo-v2.jpg'
  }
]

🧩 Recipes for modern runtimes

Because hypertag is zero-dependency, tiny, and needs no DOM, it runs anywhere JavaScript does, including edge runtimes where cheerio/jsdom won't fit. Examples use the ESM import; swap for const parse = require('hypertag') under CommonJS.

Cloudflare Workers / Vercel Edge: link-preview metadata

import parse from 'hypertag'

export default {
  async fetch(request) {
    const target = new URL(request.url).searchParams.get('url')
    const html = await fetch(target).then(res => res.text())

    const og = Object.fromEntries(
      parse(html, 'meta')
        .filter(m => m.property?.startsWith('og:'))
        .map(m => [m.property.slice(3), m.content])
    )

    return Response.json(og) // { title, image, description, ... }
  }
}

No nodejs_compat, no bundler, no polyfill. hypertag is a single dependency-free module.

Next.js: Route Handler (App Router)

// app/api/preview/route.js
import parse from 'hypertag'

export const runtime = 'edge' // optional; runs on Node too

export async function GET(request) {
  const url = new URL(request.url).searchParams.get('url')
  const html = await fetch(url).then(res => res.text())

  const meta = parse(html, 'meta')
  const pick = key => meta.find(m => m.name === key || m.property === key)?.content

  return Response.json({
    title: pick('og:title'),
    description: pick('description') ?? pick('og:description'),
    image: pick('og:image')
  })
}

Deno / Bun: favicon discovery

import parse from 'npm:hypertag' // Deno; on Bun: import parse from 'hypertag'

const html = await fetch('https://example.com').then(res => res.text())

const icons = parse(html, 'link')
  .filter(({rel}) => /\bicon\b/i.test(rel ?? ''))

console.log(icons)

🚫 When not to reach for hypertag

hypertag is an extraction primitive, not a full parser. It has no CSS selectors, no DOM traversal, and it does not repair malformed or badly nested HTML the way a spec parser does. It does not fetch URLs; you hand it an HTML string you already have. And it is not a metadata ruleset: it returns the raw <meta> and <link> tags, not the JSON-LD, Twitter Card, and oEmbed fallbacks that tools like metascraper layer on top. If you need any of those, reach for cheerio, jsdom, or metascraper.

Benchmarks 🍏🍊

Every number here is reproducible. The comparison parsers live in benchmark/, isolated from the package:

cd benchmark && npm install && npm run bench

The task is identical for every library: from a real 90 kB page, pull every <meta> and <link> tag with its attributes (all nine return the same 79 tags). One sample run on Node 24 is shown below; absolute numbers vary by machine, the ratios are the point.

Sorted by speed. Every metric is shown as a multiple of hypertag, so 1x is best and anything higher is worse.

parser speed bundle size peak memory
hypertag 11,742 ops/s · 1x 0.8 kB · 1x 49.9 MB · 1x
node-html-parser 4,444 · 2.6x slower 82.9 kB · 106x 63.2 MB · 1.3x
htmlparser2 2,941 · 4.0x slower 27.6 kB · 35x 59.0 MB · 1.2x
html5parser 2,232 · 5.3x slower 2.4 kB · 3x 57.1 MB · 1.1x
domino 1,980 · 5.9x slower 90.4 kB · 115x 81.8 MB · 1.6x
linkedom 1,031 · 11x slower 94.6 kB · 121x 72.8 MB · 1.5x
parse5 513 · 23x slower 47.2 kB · 60x 83.7 MB · 1.7x
cheerio 353 · 33x slower 489 kB · 625x 166.3 MB · 3.3x
jsdom 123 · 95x slower 775 kB · 989x 240.9 MB · 4.8x

The sharpest single number is retained heap: hypertag keeps ~0 MB (it holds no tree) versus 74.5 MB for jsdom. See benchmark/ for that column and the method.

hypertag wins by doing less: it scans the string once and returns plain objects, with no DOM or tree to build and hold. That is also the tradeoff. If you need selectors, text content, traversal, or mutation, reach for node-html-parser or cheerio (see When not to reach for hypertag above). Most run at the edge too (cheerio needs a Node-compat flag); they just cost more to ship.

About

🏎 The fastest HTML tag and attributes parser for JavaScript

Topics

Resources

Stars

30 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages