<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sean Lees</title>
    <description>The latest articles on DEV Community by Sean Lees (@seanmozeik).</description>
    <link>https://dev.to/seanmozeik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3739737%2F3badd333-a595-4bfb-90ec-c05b4b698fb7.jpg</url>
      <title>DEV Community: Sean Lees</title>
      <link>https://dev.to/seanmozeik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/seanmozeik"/>
    <language>en</language>
    <item>
      <title>Why Bun Is Ready for Production (And Better Than Node)</title>
      <dc:creator>Sean Lees</dc:creator>
      <pubDate>Thu, 29 Jan 2026 13:56:55 +0000</pubDate>
      <link>https://dev.to/seanmozeik/why-bun-is-ready-for-production-and-better-than-node-21o1</link>
      <guid>https://dev.to/seanmozeik/why-bun-is-ready-for-production-and-better-than-node-21o1</guid>
      <description>&lt;h2&gt;
  
  
  The Production Readiness Question
&lt;/h2&gt;

&lt;p&gt;"Is Bun ready for production?" gets asked on every Reddit thread about runtime alternatives. The question misses the point. Bun shipped its 1.0 in September 2023 and has been stable since. The real question is whether Bun gives you something Node doesn't.&lt;/p&gt;

&lt;p&gt;It does. Bun ships builtins that eliminate entire dependency categories. We run Bun across a mobile app's build tooling, a documentation scraping pipeline, and various CLI utilities. The dependency count dropped, the build times dropped, and the surface area for CVEs shrank along with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Package Manager Alone Is Worth It
&lt;/h2&gt;

&lt;p&gt;Before touching runtime features, consider the package manager. &lt;code&gt;bun install&lt;/code&gt; resolves and installs dependencies faster than npm, yarn, or pnpm. Our monorepo lockfile is 405 KB. A single &lt;code&gt;bun install&lt;/code&gt; handles the root Expo app, the Hono API, the TypeScript scraping pipeline, and the Nuxt marketing site.&lt;/p&gt;

&lt;p&gt;The lockfile is now JSON and Human-readable. Deterministic with SHA-512 checksums on every dependency. No binary formats to debug when things go wrong.&lt;/p&gt;

&lt;p&gt;CI pipelines feel the difference. Cold installs that took 45 seconds with npm now take 12 seconds. Warm installs with a cached lockfile finish in under 3 seconds. These numbers compound across dozens of daily builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency Elimination
&lt;/h2&gt;

&lt;p&gt;The runtime builtins matter more than the package manager speed. Here's what we replaced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;npm Package&lt;/th&gt;
&lt;th&gt;Bun Builtin&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;better-sqlite3&lt;/td&gt;
&lt;td&gt;bun:sqlite&lt;/td&gt;
&lt;td&gt;No node-gyp, no native compilation in CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zstd-codec&lt;/td&gt;
&lt;td&gt;Bun.zstdCompress&lt;/td&gt;
&lt;td&gt;No WASM overhead, sync and async variants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@aws-sdk/client-s3&lt;/td&gt;
&lt;td&gt;S3Client from "bun"&lt;/td&gt;
&lt;td&gt;Zero dependencies for R2/S3 uploads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;glob&lt;/td&gt;
&lt;td&gt;Bun.Glob&lt;/td&gt;
&lt;td&gt;Sync file discovery, no callback hell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;execa / shelljs&lt;/td&gt;
&lt;td&gt;Bun.$&lt;/td&gt;
&lt;td&gt;Tagged template shell with JSON parsing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;js-yaml&lt;/td&gt;
&lt;td&gt;YAML from "bun"&lt;/td&gt;
&lt;td&gt;Built-in YAML parsing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;toml&lt;/td&gt;
&lt;td&gt;Direct .toml imports&lt;/td&gt;
&lt;td&gt;Parsed at import time, zero runtime cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each replacement removes a dependency tree. Each dependency tree is a vector for supply chain attacks, version conflicts, and maintenance burden. The aggregate effect is a lighter, faster, more secure codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  bun:sqlite vs better-sqlite3
&lt;/h2&gt;

&lt;p&gt;SQLite without compilation is the killer feature for CI/CD pipelines. better-sqlite3 requires native bindings. Every CI run needs node-gyp, Python, and a C++ compiler. Bun's sqlite module is built into the runtime.&lt;/p&gt;

&lt;p&gt;Our documentation pipeline builds SQLite databases with FTS5 search indexing. The schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// database.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bun:sqlite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;docs.db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
  CREATE TABLE IF NOT EXISTS dl_content (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    page_name TEXT NOT NULL,
    slug TEXT NOT NULL,
    chunked_html_zst BLOB NOT NULL,
    raw_text_fts TEXT NOT NULL
  );

  CREATE INDEX IF NOT EXISTS idx_dl_content_slug ON dl_content(slug);
`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API mirrors better-sqlite3 and prepared statements work identically. The difference is deployment. Native modules need compilation, which means CI runners need build tools, and platform-specific binaries mean debugging why it works on your Mac but fails in the Docker container. bun:sqlite just runs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Use &lt;code&gt;.prepare()&lt;/code&gt; once outside loops and &lt;code&gt;.run()&lt;/code&gt; for bulk inserts. This compiles the query plan once instead of on every iteration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TypeScript support is built in. Generic query types work without additional packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pipeline.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;page_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT slug, page_name FROM dl_content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;:memory:&lt;/code&gt; URI creates transient databases for tests that disappear when the connection closes. Our test suite runs 70 test files with zero database cleanup logic because each test gets a fresh in-memory database that vanishes automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native Compression
&lt;/h2&gt;

&lt;p&gt;Bun now ships zstd and gzip compression without external dependencies. Our pipeline compresses HTML chunks with zstd at level 5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// compressor.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COMPRESSION_LEVEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compressSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zstdCompressSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;COMPRESSION_LEVEL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zstdCompress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;COMPRESSION_LEVEL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Level 5 balances compression ratio and decompression speed. Higher levels compress smaller but decompress slower. On mobile devices, decompression speed matters more than file size. A 50 KB MDN page decompresses in 5-15ms on an iPhone 14 Pro.&lt;/p&gt;

&lt;p&gt;The sync variants (&lt;code&gt;Bun.zstdCompressSync&lt;/code&gt;, &lt;code&gt;Bun.gzipSync&lt;/code&gt;) work well for batch processing, while the async variants avoid blocking when handling user requests.&lt;/p&gt;

&lt;p&gt;This replaces &lt;code&gt;zstd-codec&lt;/code&gt;, a WASM-based npm package. WASM carries overhead that native compression avoids, and when a pipeline processes thousands of HTML files during documentation builds, the aggregate time savings add up.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3Client from "bun"
&lt;/h2&gt;

&lt;p&gt;Cloudflare R2 uses the S3 API. The standard Node approach requires &lt;code&gt;@aws-sdk/client-s3&lt;/code&gt;, which pulls in dozens of transitive dependencies. Bun's native S3Client is a single import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// r2-uploader.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;S3Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bun&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;S3Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;accessKeyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accessKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;secretAccessKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.r2.cloudflarestorage.com`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dbGzPath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/gzip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Bun.file()&lt;/code&gt; integration means no manual stream handling. Pass a file reference directly to the S3 client and it handles the upload efficiently. Our &lt;code&gt;package.json&lt;/code&gt; has zero AWS SDK dependencies, and the entire R2 uploader is 50 lines of code including error handling and logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTMLRewriter
&lt;/h2&gt;

&lt;p&gt;Streaming HTML transformation without loading a full DOM tree. The API comes from Cloudflare Workers, but Bun implements it natively.&lt;/p&gt;

&lt;p&gt;Our documentation pipeline extracts base64 images from HTML, processes them separately, then restores them. HTMLRewriter handles this without parsing the entire document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// preservation.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractBase64Images&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;imageIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rewriter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HTMLRewriter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;img&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`data:image/marker;base64,DNIMG_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;imageIndex&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rewriter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;images&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTMLRewriter shines for simple, targeted transformations. We also use it to strip interactive elements from MDN documentation that don't work offline: custom elements, iframes, live code playgrounds. Chain multiple &lt;code&gt;.on()&lt;/code&gt; selectors for different removal operations.&lt;/p&gt;

&lt;p&gt;For complex DOM manipulation (creating elements, traversing node trees), linkedom remains the better tool. HTMLRewriter handles the streaming cases efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  YAML and TOML Imports
&lt;/h2&gt;

&lt;p&gt;Configuration files shouldn't require parsing libraries. Bun agrees.&lt;/p&gt;

&lt;p&gt;Both YAML and TOML are first-class citizens. You can import them directly as modules, and Bun parses at import time with hot reloading support in watch mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// metadata.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rawMetadata&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../metadata.toml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./config.yaml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Both are already JavaScript objects&lt;/span&gt;
&lt;span class="c1"&gt;// No parsing, no runtime cost&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawMetadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;javascript&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you need to parse strings (fetched content, user input), &lt;code&gt;Bun.YAML.parse&lt;/code&gt; handles it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// mdn.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yamlContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;YAML&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yamlContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our docset metadata lives in TOML files. License information, attribution text, minimum app versions. The import statement handles everything. If the file is malformed, the import fails at build time rather than crashing at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bun.$ for Shell Execution
&lt;/h2&gt;

&lt;p&gt;This one deserves attention because it replaces more than just execa. Every project accumulates shell scripts and Python one-offs: a bash script to sync database backups, a Python script to generate license files, a shell script to run wrangler commands with the right flags. These scripts work, but they live outside your main codebase, lack type checking, and require different tooling to maintain.&lt;/p&gt;

&lt;p&gt;Bun.$ lets you write all of that in TypeScript. Tagged template shell commands with ergonomic output handling. The following is from a script to generate our App Store screenshots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// appstore-icon-grid.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bun&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Convert SVG to PNG at specific dimensions&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;`rsvg-convert -w &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pngWidth&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -h &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pngHeight&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;svgPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -o &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pngPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ImageMagick for blur effects&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;`magick &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pngPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -adaptive-blur 0x16 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blurredPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Composite images with a gradient mask&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;`magick &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pngPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blurredPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;maskPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -composite &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pngPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Optimize and cleanup&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;`oxipng -o 4 -s &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pngPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quiet&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;`rm &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blurredPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;maskPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Variables interpolate safely into the template, &lt;code&gt;.quiet()&lt;/code&gt; suppresses stdout for clean logs, and the whole pipeline lives in a TypeScript file with the rest of your codebase. When a path changes or a flag needs adjustment, you refactor code instead of hunting through shell scripts.&lt;/p&gt;

&lt;p&gt;Compare this to execa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// execa approach&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execa&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;execa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;stdout&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bunx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;license-checker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;licenseInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Bun approach&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;licenseInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;`bunx license-checker --production --json`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Template literals feel natural for shell scripting, variables interpolate safely, and the API surface is smaller than execa's while producing cleaner output.&lt;/p&gt;

&lt;h2&gt;
  
  
  And That's Not All
&lt;/h2&gt;

&lt;p&gt;Quick hits on other builtins we use daily:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bun.hash / xxHash64&lt;/strong&gt; generates content hashes for deduplication. We hash SVGs and documentation chunks to avoid storing duplicates. One line: &lt;code&gt;Bun.hash.xxHash64(input).toString(16)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bun.serve&lt;/strong&gt; creates HTTP servers with Unix socket support. Our language detection service runs on a Unix socket for fast IPC between TypeScript and Python workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;bun:test&lt;/strong&gt; runs our 70 test files with zero configuration. Jest-compatible API, faster execution. The bunfig.toml for our test setup is four lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bun.Glob&lt;/strong&gt; handles file pattern matching. &lt;code&gt;new Glob("**/*.html").scanSync()&lt;/code&gt; finds files without callback gymnastics. Replaces the glob npm package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bun.file / Bun.write&lt;/strong&gt; are cleaner than fs. &lt;code&gt;await Bun.file(path).text()&lt;/code&gt; reads a file. &lt;code&gt;await Bun.write(path, content)&lt;/code&gt; writes it. The API is obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bun.sleep&lt;/strong&gt; is a promisified delay. &lt;code&gt;await Bun.sleep(100)&lt;/code&gt; pauses for 100ms. We use it to rate-limit documentation fetches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating a Pipeline from Python
&lt;/h2&gt;

&lt;p&gt;We're in the middle of migrating our entire documentation scraping pipeline from Python to TypeScript on Bun. The Python version is 9,500 lines across 60 files with dependencies that Bun and TypeScript now replace: &lt;code&gt;pydantic&lt;/code&gt; becomes TypeScript interfaces, &lt;code&gt;sqlmodel&lt;/code&gt; becomes bun:sqlite, &lt;code&gt;boto3&lt;/code&gt; becomes the native S3Client, &lt;code&gt;httpx&lt;/code&gt; and &lt;code&gt;requests&lt;/code&gt; become fetch, &lt;code&gt;xxhash&lt;/code&gt; becomes Bun.hash, &lt;code&gt;rustoml&lt;/code&gt; becomes direct TOML imports, and &lt;code&gt;nh3&lt;/code&gt; (a Rust-based HTML sanitizer) becomes HTMLRewriter. Even &lt;code&gt;trio&lt;/code&gt; for async and &lt;code&gt;orjson&lt;/code&gt; for fast JSON are unnecessary when the runtime handles both natively.&lt;/p&gt;

&lt;p&gt;The TypeScript version consolidates everything into the same language as our mobile app. The same type definitions that describe our SQLite schema in the React Native app also describe the schema in the build pipeline. When we change a column name, TypeScript catches the mismatch everywhere.&lt;/p&gt;

&lt;p&gt;Building bespoke scrapers turned out to be faster in TypeScript than Python. Bun's native fetch replaces httpx, HTMLRewriter handles streaming transformations faster than BeautifulSoup ever could, and we use linkedom when we need full DOM manipulation. Extracting navigation trees from documentation sites with Playwright feels natural when your scraping code and your app code share the same language. The CLI uses @clack/prompts for interactive multiselects and spinners that update as the pipeline progresses, which is a nicer experience than Click ever provided.&lt;/p&gt;

&lt;p&gt;The migration also eliminates cross-language friction. The Python pipeline used SQLModel with Pydantic for database access, a heavyweight ORM for what amounts to simple inserts and queries. The TypeScript version uses bun:sqlite directly with raw SQL, no ORM overhead. The Python version used a WASM-based zstd codec. The TypeScript version uses native Bun compression. Each boundary we removed was a source of subtle bugs and deployment complexity.&lt;/p&gt;

&lt;p&gt;We're not finished yet. The TypeScript pipeline handles MDN, Swift, Nuxt, and several other documentation sources, but some edge cases remain in Python. The architecture is proven, though, and the trajectory is clear: one language, one runtime, one set of tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Node Still Wins
&lt;/h2&gt;

&lt;p&gt;Production-ready doesn't mean perfect. Know when to fall back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serverless platforms haven't caught up.&lt;/strong&gt; Cloudflare Workers doesn't support the Bun runtime. Our API runs on Hono, which works great on Workers, but we can't use Bun.serve or bun:sqlite in that environment. The serverless ecosystem remains Node-first across the board, from AWS Lambda to Vercel Functions to Cloudflare Workers, and Bun's deploy story is weaker as a result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Immaturity gaps exist.&lt;/strong&gt; Bun is production-ready but not bug-free. Our Nuxt marketing site can't use the &lt;code&gt;--bun&lt;/code&gt; flag because of a longstanding compatibility issue with ultrahtml, which means the entire site runs on Node because of one dependency conflict. The ecosystem is smaller, obscure packages sometimes break, and when they do you're debugging unfamiliar territory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The workaround is hybrid deployment.&lt;/strong&gt; Our scraping pipeline runs on Bun, our API deploys to Workers (Node-compatible), and our marketing site runs on Node. Each component uses the best available runtime for its deployment target. Dogma about "all Bun" or "all Node" misses the point. Ship what works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Bun keeps shipping. Recent versions added native S3 support, improved Windows compatibility, and better Node.js API coverage. The trajectory suggests more dependency elimination ahead.&lt;/p&gt;

&lt;p&gt;The question isn't whether Bun is ready. Bun has been ready. The question is whether your deployment targets support it. If they do, the dependency reduction and developer experience improvements are substantial. If they don't, Bun's package manager and local development speed still make it worth installing.&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://docnative.app" rel="noopener noreferrer"&gt;DocNative's&lt;/a&gt; documentation pipeline entirely on Bun. The scraping, cleaning, database building, compression, and upload steps use native APIs throughout, which meant no better-sqlite3 compilation, no WASM compression overhead, and no AWS SDK dependency trees to manage. The pipeline processes MDN, Swift, Python, and React documentation into offline-ready SQLite databases. Pages load in under 200ms on a phone because we optimized the entire chain, and Bun made that optimization possible without accumulating dependencies.&lt;/p&gt;

&lt;p&gt;The app ships documentation that works on airplanes, subways, and anywhere else the network doesn't. The phone never touches raw HTML; it queries a local database, decompresses a chunk, and renders. Bun built the databases, and the builtins made it clean.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Out Now:&lt;/strong&gt; Bun and TypeScript documentation are both included in DocNative at launch. Read the APIs that built the app, offline.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>bunjs</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why HTML Tables Break on React Native (And How We Fixed It)</title>
      <dc:creator>Sean Lees</dc:creator>
      <pubDate>Thu, 29 Jan 2026 13:46:34 +0000</pubDate>
      <link>https://dev.to/seanmozeik/why-html-tables-break-on-react-native-and-how-we-fixed-it-4b9d</link>
      <guid>https://dev.to/seanmozeik/why-html-tables-break-on-react-native-and-how-we-fixed-it-4b9d</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;HTML tables make an assumption: a layout engine exists that will calculate column widths automatically. CSS tables measure all cells, determine the longest content in each column, distribute remaining space, and render everything in one pass. The browser handles this transparently.&lt;/p&gt;

&lt;p&gt;React Native has no such engine.&lt;/p&gt;

&lt;p&gt;When you feed an HTML table to &lt;code&gt;react-native-render-html&lt;/code&gt;, each cell renders independently. There is no coordinated pass where columns agree on widths. The result is predictable: columns collapse, text overflows, rows misalign. A table that looks correct in Safari becomes unreadable on iOS.&lt;/p&gt;

&lt;p&gt;We needed tables to work. MDN documentation contains hundreds of them. Browser compatibility tables, API reference tables, method signature tables. Skip tables and you skip a significant chunk of the documentation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why Not Simpler Solutions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WebView would have worked. Embed a browser, let it handle tables. But the app is called Doc*Native* for a reason. WebViews drag in scroll conflicts, memory bloat, and that unmistakable feel of a webpage stuffed inside a shell.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;react-native-render-html&lt;/code&gt; library ships a &lt;a href="https://github.com/native-html/plugins/tree/master/packages/heuristic-table-plugin#readme" rel="noopener noreferrer"&gt;heuristic table plugin&lt;/a&gt;. It guesses column widths. Simple tables render fine. MDN's 30-column browser compatibility grids do not. Python method signatures with nested code blocks do not. Columns collapsed to nothing. Rows overflowed without scrolling. Heuristics fail when the input is hostile. Documentation tables are hostile.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why It Happens
&lt;/h2&gt;

&lt;p&gt;React Native uses Yoga, a Flexbox implementation. Yoga understands flex containers, flex items, and percentage widths. It does not understand &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt;, or &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt;. When &lt;code&gt;react-native-render-html&lt;/code&gt; encounters a table, it maps these elements to Views with flex properties. Each cell becomes a flex item.&lt;/p&gt;

&lt;p&gt;The mapping fails because HTML table layout is not Flexbox. Table layout has two passes: constraint collection and width distribution. Flexbox has one pass: children size themselves, parents adjust. A cell with &lt;code&gt;width: auto&lt;/code&gt; in HTML will expand to fit content and then negotiate with siblings. A flex item with no explicit width will compress or expand based on &lt;code&gt;flexGrow&lt;/code&gt; and &lt;code&gt;flexShrink&lt;/code&gt;, ignoring content entirely.&lt;/p&gt;

&lt;p&gt;The specific failures:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Column width disagreement.&lt;/strong&gt; Row 1 has a short cell; Row 2 has a long cell. Without coordination, each row picks different widths. Columns zigzag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long words overflow.&lt;/strong&gt; German compound nouns, API method names, URLs. These exceed the screen width and clip without wrapping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text wrapping breaks mid-word.&lt;/strong&gt; The system picks arbitrary break points, producing "funct-" on one line and "ion" on the next.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bold headers compress.&lt;/strong&gt; Headers use &lt;code&gt;font-weight: 600&lt;/code&gt;, which renders wider than regular text. Width calculations that ignore this produce truncated headers.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Phase 1: Estimation
&lt;/h2&gt;

&lt;p&gt;Our solution begins before any cell renders. We extract the table structure from the parsed HTML and estimate column constraints by analyzing text content before measurement or rendering.&lt;/p&gt;

&lt;p&gt;The estimation uses character width coefficients:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Content Type&lt;/th&gt;
&lt;th&gt;Coefficient&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Regular text&lt;/td&gt;
&lt;td&gt;0.55&lt;/td&gt;
&lt;td&gt;Proportional fonts average 55% of font size per character&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold header&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;36% wider due to font-weight: 600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monospace&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;Fixed-width characters are wider than proportional&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a header cell containing "Parameters" at 16px font size, the estimated width is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;charWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 12px per character&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;naturalWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 120px for 10 characters&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs for every cell in the table. We collect two values per column: &lt;code&gt;minWidth&lt;/code&gt; (the longest unbreakable word) and &lt;code&gt;naturalWidth&lt;/code&gt; (the longest line if given infinite space).&lt;/p&gt;

&lt;p&gt;The minimum width calculation respects soft hyphens. These Unicode characters (&lt;code&gt;\u00AD&lt;/code&gt;) mark valid break points inserted at build time. A word like &lt;code&gt;documentation&lt;/code&gt; might be stored as &lt;code&gt;docu-men-ta-tion&lt;/code&gt;, allowing the renderer to break at syllable boundaries. We insert soft-hyphens into content at build time in our documentation cleaning pipeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// computeWidths.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;syllables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;u00AD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxBreaks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetSegments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxBreaks&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minSegmentLen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;targetSegments&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Short words get one break maximum. Long words (16+ characters) get two. This prevents excessive fragmentation while still allowing wrapping where it helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Overflow Decision
&lt;/h2&gt;

&lt;p&gt;After estimation, we know the total minimum width required: the sum of every column's &lt;code&gt;minWidth&lt;/code&gt;. Compare this to the available screen width (device width minus 64px padding).&lt;/p&gt;

&lt;p&gt;If the content fits, we use Flexbox. Each column gets &lt;code&gt;flex: [calculatedWidth]&lt;/code&gt;, distributing space proportionally. The table expands to fill the container.&lt;/p&gt;

&lt;p&gt;If the content exceeds available width, we switch strategies. Each column gets an explicit &lt;code&gt;width&lt;/code&gt; value, and the table sits inside a horizontal &lt;code&gt;ScrollView&lt;/code&gt;. Flexbox does not work correctly inside scroll containers; explicit widths do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CellRenderer.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cellStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;needsScroll&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;      &lt;span class="c1"&gt;// Explicit width for scroll&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// Proportional flex when fitting&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This decision happens once per table, before rendering begins. Cells never switch strategies mid-render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Width Distribution
&lt;/h2&gt;

&lt;p&gt;When content fits, we distribute extra space intelligently. The algorithm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with each column at its minimum width.&lt;/li&gt;
&lt;li&gt;Calculate how much extra space each column wants (natural width minus minimum width).&lt;/li&gt;
&lt;li&gt;Distribute available extra space proportionally to demand.&lt;/li&gt;
&lt;li&gt;Cap each column at its natural width. No column grows beyond what its content requires.&lt;/li&gt;
&lt;li&gt;If any column hits its cap, redistribute its unused allocation to columns still below their caps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This iterative redistribution continues until either all extra space is allocated or no column can accept more.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; The iteration has a 1px threshold. If less than 1px remains to distribute, the loop exits. This prevents floating-point precision issues from causing infinite loops.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For scrollable tables, the logic differs. We distribute width proportionally to total content volume (the sum of all &lt;code&gt;naturalWidth&lt;/code&gt; values in each column). A column containing dense code examples gets more space than a column of short labels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Colspan Handling
&lt;/h2&gt;

&lt;p&gt;Tables with merged cells (&lt;code&gt;colspan&lt;/code&gt;) complicate everything. A cell spanning three columns does not tell us anything about individual column widths. It only constrains the sum.&lt;/p&gt;

&lt;p&gt;Our approach: distribute colspan constraints only when necessary. If a spanning cell requires more width than the covered columns already provide, we distribute the excess proportionally based on each column's flex weight.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// computeWidths.ts&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cellMinWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;existingMinTotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;excess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cellMinWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;existingMinTotal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;colspan&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;startColumn&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;share&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;excess&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flexWeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalWeight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;share&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the spanned columns already satisfy the requirement, we leave them alone. This prevents colspans from inflating widths unnecessarily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedded Content
&lt;/h2&gt;

&lt;p&gt;Tables contain more than text. Code blocks, images, SVG diagrams. These elements have their own width requirements that character counting cannot predict.&lt;/p&gt;

&lt;p&gt;We handle this with a callback system. Embedded renderers (CodeRenderer, ImageRenderer, SVGRenderer) report their measured dimensions after mounting. The table system receives these reports and updates column constraints accordingly.&lt;/p&gt;

&lt;p&gt;A complication: embedded content renders after the initial layout pass. The table might finalize widths before an embedded code block reports its true size. We handle this with re-measurement.&lt;/p&gt;

&lt;p&gt;When an embedded element reports a size more than 10% different from the estimate, the table triggers a new measurement cycle. This recomputes widths and re-renders cells with updated values.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; Re-measurement is capped at 3 iterations. Some content produces unstable measurements (an image that resizes based on container width, for example). The cap prevents infinite re-render loops.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pre-Population
&lt;/h2&gt;

&lt;p&gt;Performance matters. Users scroll through documentation quickly. Tables need to render fast.&lt;/p&gt;

&lt;p&gt;The naive approach: render cells, let each register itself, then compute widths. This creates a visible flash: cells appear at wrong widths, then jump to correct positions.&lt;/p&gt;

&lt;p&gt;We eliminated the flash with pre-population. Before any cell component mounts, we analyze the parsed table structure and register all cells synchronously. Width computation happens before the first render pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TableMeasurementContext.tsx&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;estimating&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;hasPrePopulated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allRows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...(&lt;/span&gt;&lt;span class="nx"&gt;tableStructure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thead&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;tableStructure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;allRows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cell&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;estimates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimateConstraints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;cellsRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;estimates&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;hasPrePopulated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tableStructure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cell components still register when they mount, but they find their data already populated. No delays, no second passes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Viewport Adaptation
&lt;/h2&gt;

&lt;p&gt;Phone screens vary. A table comfortable on iPad becomes cramped on iPhone SE. We adapt minimum column widths to viewport size:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Breakpoint&lt;/th&gt;
&lt;th&gt;Width&lt;/th&gt;
&lt;th&gt;Min Column&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Phone portrait&lt;/td&gt;
&lt;td&gt;&amp;lt; 400px&lt;/td&gt;
&lt;td&gt;60px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phone landscape&lt;/td&gt;
&lt;td&gt;&amp;lt; 600px&lt;/td&gt;
&lt;td&gt;80px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tablet portrait&lt;/td&gt;
&lt;td&gt;&amp;lt; 900px&lt;/td&gt;
&lt;td&gt;100px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tablet landscape&lt;/td&gt;
&lt;td&gt;≥ 900px&lt;/td&gt;
&lt;td&gt;120px&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Small screens get aggressive minimums. Columns compress further before triggering horizontal scroll. Large screens get relaxed minimums. Content breathes.&lt;/p&gt;

&lt;p&gt;Typography scales too. Phone portrait uses 90% font size with 6px cell padding. Tablets use full size with 10px padding. These adjustments happen automatically based on device characteristics detected at render time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;The system handles MDN's browser compatibility tables, which contain 30+ columns of icons and version numbers. It handles Python's method signature tables, with long parameter names and type annotations. It handles tables inside tables (yes, documentation does this).&lt;/p&gt;

&lt;p&gt;Tables load without layout flashes. Scrollable tables indicate their scrollability with natural momentum. Soft hyphens break long words at sensible syllable boundaries.&lt;/p&gt;

&lt;p&gt;The code handles edge cases we did not anticipate: tables with empty rows (filtered out), tables with empty columns (filtered out), single-cell tables (unwrapped entirely), tables with only headers (rendered normally).&lt;/p&gt;

&lt;p&gt;The key insight: React Native forces you to solve problems that browsers solve invisibly. There is no layout engine to lean on. You measure, compute, and render with full knowledge of what you are doing. The result is a table renderer that works the way tables should work, rebuilt from first principles for a platform that has none.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is the rendering engine behind &lt;a href="https://docnative.app" rel="noopener noreferrer"&gt;DocNative&lt;/a&gt;. When you read the &lt;code&gt;Array.prototype.map&lt;/code&gt; documentation on your phone, the parameter tables render through this measurement system. The Rust standard library tables with their function signatures. The CSS property tables with browser support.&lt;/p&gt;

&lt;p&gt;DocNative ships with docsets including MDN, Python, React, TypeScript, and Go. Every table in every docset passes through this pipeline. The documentation loads offline, scrolls at 60fps, and renders tables that actually look like tables. No network required. No browser engine required. Just the documentation you need, formatted correctly.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
