<?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>Forem: Francesco Di Donato</title>
    <description>The latest articles on Forem by Francesco Di Donato (@didof).</description>
    <link>https://forem.com/didof</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%2F334733%2Fa78bbefe-a70a-495b-8cb9-add8f7d8e4e1.jpg</url>
      <title>Forem: Francesco Di Donato</title>
      <link>https://forem.com/didof</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/didof"/>
    <language>en</language>
    <item>
      <title>How I Turned 4 Sites and a Shared Lib Into One pnpm Workspace</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Tue, 14 Apr 2026 22:00:00 +0000</pubDate>
      <link>https://forem.com/didof/how-i-turned-4-sites-and-a-shared-lib-into-one-pnpm-workspace-3l75</link>
      <guid>https://forem.com/didof/how-i-turned-4-sites-and-a-shared-lib-into-one-pnpm-workspace-3l75</guid>
      <description>&lt;p&gt;Before the monorepo, my local &lt;code&gt;~/Workspace/didof/&lt;/code&gt; looked like a cork board of unrelated projects: four Astro sites with their own &lt;code&gt;node_modules&lt;/code&gt; folders, their own lockfiles, their own &lt;code&gt;@didof/shared-something-that-was-always-out-of-sync&lt;/code&gt;, and their own build commands. Pushing a fix to the shared library meant four &lt;code&gt;npm link&lt;/code&gt; dances, four CI builds, and a lingering anxiety that one of the sites was running a three-week-old version of whatever I just edited.&lt;/p&gt;

&lt;p&gt;Two weeks ago I rolled all of that into a single pnpm workspace. Four Astro apps (&lt;a href="https://didof.dev" rel="noopener noreferrer"&gt;didof.dev&lt;/a&gt;, &lt;a href="https://velocaption.com" rel="noopener noreferrer"&gt;velocaption.com&lt;/a&gt;, &lt;a href="https://speechstudio.ai" rel="noopener noreferrer"&gt;speechstudio.ai&lt;/a&gt;, &lt;a href="https://linkpreview.ai" rel="noopener noreferrer"&gt;linkpreview.ai&lt;/a&gt;) plus one shared library (&lt;code&gt;@didof/shared&lt;/code&gt;) now live under one &lt;code&gt;packages/&lt;/code&gt; folder, share one lockfile, and can cross-reference each other's source at build time. I'm shipping faster, and the cross-version bugs are gone.&lt;/p&gt;

&lt;p&gt;This post is the walkthrough, with every gotcha I actually hit.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; is a three-line file that declares which folders are part of the workspace. That's the whole mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;workspace:*&lt;/code&gt; in a dependency spec points at a sibling package. No publishing, no &lt;code&gt;npm link&lt;/code&gt;, no version drift.&lt;/li&gt;
&lt;li&gt;Run any script across packages with &lt;code&gt;pnpm --filter &amp;lt;name&amp;gt; &amp;lt;script&amp;gt;&lt;/code&gt;. Combine filters for graph-aware builds.&lt;/li&gt;
&lt;li&gt;Subpath &lt;code&gt;exports&lt;/code&gt; in a sibling's &lt;code&gt;package.json&lt;/code&gt; lets another package import just one module (e.g. a registry file) without pulling in the whole site.&lt;/li&gt;
&lt;li&gt;The five gotchas at the end of this post cost me roughly four hours combined. All of them are cheap to fix once you know about them.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why pnpm, Not npm or Yarn
&lt;/h2&gt;

&lt;p&gt;npm workspaces work. Yarn workspaces work. I use pnpm because of how it handles &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every package in a pnpm workspace gets its own &lt;code&gt;node_modules&lt;/code&gt; folder, but the contents are &lt;strong&gt;symlinks&lt;/strong&gt; into a single content-addressable store at the monorepo root. One copy of React on disk, referenced by every package that depends on React. With four Astro sites each pulling in the same ~800 MB of Astro + Vite + React + Radix + Tailwind, the savings are dramatic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; node_modules          &lt;span class="c"&gt;# content store at root&lt;/span&gt;
1.2G
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; packages/&lt;span class="k"&gt;*&lt;/span&gt;/node_modules  &lt;span class="c"&gt;# per-package symlink farms&lt;/span&gt;
4.8M    packages/didof.dev/node_modules
3.2M    packages/velocaption.com/node_modules
3.1M    packages/speechstudio.ai/node_modules
2.9M    packages/linkpreview.ai/node_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I had four separate &lt;code&gt;node_modules&lt;/code&gt; folders with copies of the same stuff, that's closer to 5 GB. pnpm reduces it to 1.2 GB. On a laptop with 500 GB of storage and a tendency to hoard Docker images, that matters.&lt;/p&gt;

&lt;p&gt;The other reason is &lt;strong&gt;strict dependency resolution&lt;/strong&gt;. npm and Yarn will let your code import any package that happens to be in &lt;code&gt;node_modules&lt;/code&gt;, including transitive dependencies your project never declared. Everything works locally, everything works in CI, and then one day a parent dependency drops that transitive dep in a minor version bump and your build breaks in production with no diff to blame.&lt;/p&gt;

&lt;p&gt;Here's a concrete case I lived with Yarn on the old didof.dev repo. A Vite plugin I used depended on &lt;code&gt;lodash&lt;/code&gt; internally. Somewhere in my source I wrote:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;debounce&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;lodash/debounce&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I never added &lt;code&gt;lodash&lt;/code&gt; to my own &lt;code&gt;package.json&lt;/code&gt;. It worked for months. Then the Vite plugin upgraded to &lt;code&gt;lodash-es&lt;/code&gt;, dropped the old dep, and my build broke:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Could not resolve "lodash/debounce" from src/hooks/useDebounce.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With pnpm, this mistake surfaces the moment you write the import. pnpm's &lt;code&gt;node_modules&lt;/code&gt; layout only exposes packages you directly declare — transitive ones are hidden inside &lt;code&gt;.pnpm/&lt;/code&gt;. So the first run of &lt;code&gt;pnpm dev&lt;/code&gt; after adding that import fails immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[plugin:vite:import-analysis] Failed to resolve import "lodash/debounce" from "src/hooks/useDebounce.ts"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same error, six months earlier, when it's a two-minute fix (&lt;code&gt;pnpm add lodash&lt;/code&gt;) instead of a production incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Layout That Actually Works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content-hub/
├── pnpm-workspace.yaml
├── package.json          # root; devDeps shared across packages
├── pnpm-lock.yaml        # single lockfile for everything
├── scripts/              # monorepo-wide scripts
├── packages/
│   ├── shared/           # @didof/shared: plugins, schemas, utils
│   ├── didof.dev/
│   ├── velocaption.com/
│   ├── speechstudio.ai/
│   └── linkpreview.ai/
└── .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every package has its own &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;astro.config.mjs&lt;/code&gt;, &lt;code&gt;tsconfig.json&lt;/code&gt;, &lt;code&gt;src/&lt;/code&gt;, and &lt;code&gt;dist/&lt;/code&gt;. The root &lt;code&gt;package.json&lt;/code&gt; is a thin shell that holds monorepo-wide dev tooling (tsx, typescript, sharp, glob) and a handful of scripts that delegate into packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev:didof"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm --filter didof.dev dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev:velocaption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm --filter velocaption.com dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build:all"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm -r build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sync"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm tsx scripts/content-sync.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ship:didof"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm --filter didof.dev ship"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ship:velocaption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm --filter velocaption.com ship"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern repeats for every workspace member: one &lt;code&gt;dev:&amp;lt;name&amp;gt;&lt;/code&gt; and one &lt;code&gt;ship:&amp;lt;name&amp;gt;&lt;/code&gt; per site. Cross-site commands (&lt;code&gt;build:all&lt;/code&gt;, &lt;code&gt;sync&lt;/code&gt;) stay unqualified since they operate on the graph.&lt;/p&gt;

&lt;p&gt;No clever turbo orchestration. Just pnpm filters. For four sites that mostly build independently, Turborepo would be over-engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  pnpm-workspace.yaml: The Three-Line File That Starts It All
&lt;/h2&gt;

&lt;p&gt;This is the entire contents of &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; at the repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;packages/*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole mechanism. Anything matching that glob is a workspace member. When I cloned &lt;code&gt;linkpreview.ai&lt;/code&gt; into &lt;code&gt;packages/linkpreview.ai/&lt;/code&gt; and ran &lt;code&gt;pnpm install&lt;/code&gt;, pnpm discovered it, linked its declared deps, and exposed it under its declared package name. No further registration needed.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;packages&lt;/code&gt; key accepts multiple globs if you want more specific grouping (&lt;code&gt;apps/*&lt;/code&gt;, &lt;code&gt;libs/*&lt;/code&gt;, &lt;code&gt;tooling/*&lt;/code&gt;). For a flat layout one glob is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing Code: &lt;code&gt;workspace:*&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The shared library lives at &lt;code&gt;packages/shared/&lt;/code&gt; with this identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@didof/shared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.0.1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any site that wants to use it declares a dependency with the &lt;code&gt;workspace:*&lt;/code&gt; protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"didof.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@didof/shared"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workspace:*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;*&lt;/code&gt; means "whatever version is in the workspace right now." On &lt;code&gt;pnpm install&lt;/code&gt;, pnpm creates a symlink from &lt;code&gt;packages/didof.dev/node_modules/@didof/shared&lt;/code&gt; pointing at &lt;code&gt;packages/shared/&lt;/code&gt;. Edits in shared show up instantly in every consumer: no rebuild, no publish, no &lt;code&gt;npm link&lt;/code&gt; of the day.&lt;/p&gt;

&lt;p&gt;When you actually publish the workspace (say, releasing &lt;code&gt;@didof/shared&lt;/code&gt; to npm for public consumption), pnpm rewrites &lt;code&gt;workspace:*&lt;/code&gt; to the real version number at pack time. The published &lt;code&gt;package.json&lt;/code&gt; looks normal to any consumer outside the workspace. Inside the workspace, development stays symlinked.&lt;/p&gt;

&lt;p&gt;Here's what &lt;code&gt;packages/shared/package.json&lt;/code&gt;'s &lt;code&gt;exports&lt;/code&gt; map looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@didof/shared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./index.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./plugins/index.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./schemas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./schemas/index.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./astro-config-base"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./astro-config-base.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./components/CodeBlockScript.astro"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./components/CodeBlockScript.astro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./styles/codeblock.css"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./styles/codeblock.css"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumers import specific entrypoints:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseBlogSchema&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;@didof/shared/schemas&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;codeblockCopyTransformer&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;@didof/shared/plugins&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@didof/shared/styles/codeblock.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each subpath maps to a specific file. You keep the barrel-import ergonomics without pulling the whole library into a bundle when only one piece is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Workspace From Scratch
&lt;/h2&gt;

&lt;p&gt;If you're starting from zero (or consolidating existing repos the way I did), the bootstrap is maybe a dozen commands. Here's the full sequence.&lt;/p&gt;

&lt;p&gt;Create the root and init a package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;content-hub &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;content-hub
pnpm init
git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Declare which folders are workspace members:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; pnpm-workspace.yaml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
packages:
  - "packages/*"
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Move each existing project into &lt;code&gt;packages/&amp;lt;name&amp;gt;&lt;/code&gt;. For a greenfield package, &lt;code&gt;mkdir packages/&amp;lt;name&amp;gt;&lt;/code&gt; and &lt;code&gt;pnpm init&lt;/code&gt; inside it. Either way pnpm discovers them on the next install.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;packages
&lt;span class="nb"&gt;mv&lt;/span&gt; ../didof.dev packages/
&lt;span class="nb"&gt;mv&lt;/span&gt; ../velocaption.com packages/
&lt;span class="c"&gt;# ... one mv per project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every moved project keeps its own &lt;code&gt;package.json&lt;/code&gt;, but its individual &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; and &lt;code&gt;node_modules&lt;/code&gt; are obsolete. Clean them so the monorepo-root lockfile takes over:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; packages/&lt;span class="k"&gt;*&lt;/span&gt;/node_modules packages/&lt;span class="k"&gt;*&lt;/span&gt;/pnpm-lock.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install everything from the new root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify pnpm found every package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--depth&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prints each workspace member with its declared dependencies. If a package is missing, check that its &lt;code&gt;package.json&lt;/code&gt; sits directly in &lt;code&gt;packages/&amp;lt;name&amp;gt;/&lt;/code&gt;, not nested inside another folder.&lt;/p&gt;

&lt;p&gt;From here, &lt;code&gt;workspace:*&lt;/code&gt; references resolve automatically, &lt;code&gt;--filter&lt;/code&gt; scripts work, and shared dev tooling goes at the root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; typescript tsx sharp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire bootstrap. Fewer moving parts than most single-project setups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Scripts with &lt;code&gt;--filter&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;--filter&lt;/code&gt; flag is where most of my daily work happens. A few patterns that earn their keep:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run dev on one site:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; didof.dev dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Build every package in topological order&lt;/strong&gt; (respects &lt;code&gt;workspace:*&lt;/code&gt; deps):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;-r&lt;/span&gt; build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pnpm sees that &lt;code&gt;didof.dev&lt;/code&gt; depends on &lt;code&gt;@didof/shared&lt;/code&gt; and builds shared first. If you swap pnpm for npm workspaces you'd need to orchestrate this yourself or rely on a runner like Turborepo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run a command in every package that matches a pattern:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"./packages/*"&lt;/span&gt; check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Add a dependency to a specific package:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; didof.dev add @radix-ui/react-dialog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Add a root-level dev dependency&lt;/strong&gt; (the &lt;code&gt;-w&lt;/code&gt; flag for workspace root):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; sharp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one is the first gotcha I hit. More on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subpath Exports Across Packages
&lt;/h2&gt;

&lt;p&gt;Here's where monorepos start earning their keep for real.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;packages/velocaption.com/&lt;/code&gt; has a tool registry at &lt;code&gt;src/features/tools/registry.ts&lt;/code&gt; that exports a &lt;code&gt;TOOL_REGISTRY&lt;/code&gt; object with 16 entries. My &lt;code&gt;didof.dev&lt;/code&gt; landing page wants to show a subset of those tools as a "Free Tools by Velocaption" section.&lt;/p&gt;

&lt;p&gt;Before the monorepo, the only way to do this was to fetch &lt;code&gt;https://velocaption.com/tools/rss.xml&lt;/code&gt; at build time, parse the XML, and hope the deployed site was up. Now I add a subpath export to velocaption's &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"velocaption.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./tools-registry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/features/tools/registry.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it as a workspace dep in &lt;code&gt;didof.dev&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"velocaption.com"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workspace:*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now didof.dev imports the registry directly:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TOOL_REGISTRY&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;velocaption.com/tools-registry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Astro/Vite resolves the TypeScript at build time. No compilation step, no JSON generation, no HTTP request to production. The landing page renders from the same source of truth the velocaption site uses for its own tool pages.&lt;/p&gt;

&lt;p&gt;For content that's a little more shape-y (like blog posts with frontmatter), I wrote a small build-time aggregator that globs sibling &lt;code&gt;src/content/blog/*/index.mdx&lt;/code&gt;, parses frontmatter, copies cover images into didof.dev's public folder, and emits JSON. The pattern is the same: &lt;strong&gt;filesystem reads, not HTTP&lt;/strong&gt;. Sibling packages are just code and data on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Gotchas
&lt;/h2&gt;

&lt;p&gt;These cost me time. They shouldn't cost you any.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;pnpm deploy&lt;/code&gt; is a built-in, not your &lt;code&gt;deploy&lt;/code&gt; script
&lt;/h3&gt;

&lt;p&gt;I wrote a &lt;code&gt;ship&lt;/code&gt; npm script that chained &lt;code&gt;pnpm check &amp;amp;&amp;amp; pnpm build &amp;amp;&amp;amp; pnpm deploy&lt;/code&gt;. The &lt;code&gt;pnpm deploy&lt;/code&gt; step failed every time with &lt;code&gt;ERR_PNPM_NOTHING_TO_DEPLOY&lt;/code&gt;. Took me ten minutes to find out: &lt;code&gt;pnpm deploy&lt;/code&gt; is a pnpm subcommand that copies a package's production dependencies somewhere. It doesn't run my &lt;code&gt;deploy&lt;/code&gt; npm script.&lt;/p&gt;

&lt;p&gt;The fix is &lt;code&gt;pnpm run deploy&lt;/code&gt; (explicit script invocation):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wrangler pages deploy dist --project-name didof-dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ship"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm check &amp;amp;&amp;amp; pnpm build &amp;amp;&amp;amp; pnpm run deploy"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any script name that collides with a pnpm built-in needs &lt;code&gt;pnpm run &amp;lt;name&amp;gt;&lt;/code&gt; to disambiguate. The built-in commands to watch out for: &lt;code&gt;deploy&lt;/code&gt;, &lt;code&gt;install&lt;/code&gt;, &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;remove&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;link&lt;/code&gt;, &lt;code&gt;pack&lt;/code&gt;, &lt;code&gt;publish&lt;/code&gt;, &lt;code&gt;start&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;. Most of those are obvious (don't override &lt;code&gt;install&lt;/code&gt;), but &lt;code&gt;deploy&lt;/code&gt; and &lt;code&gt;start&lt;/code&gt; are common enough to conflict.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;pnpm add&lt;/code&gt; without &lt;code&gt;-w&lt;/code&gt; puts deps in the wrong place
&lt;/h3&gt;

&lt;p&gt;Running &lt;code&gt;pnpm add sharp&lt;/code&gt; from the monorepo root looks like it should add sharp to the root's &lt;code&gt;package.json&lt;/code&gt;. It does not. pnpm will refuse, or worse, add it to a random package depending on which directory you're in.&lt;/p&gt;

&lt;p&gt;To add to the root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; sharp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-w&lt;/code&gt; is short for &lt;code&gt;--workspace-root&lt;/code&gt;. To add to a specific package from anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; didof.dev add @radix-ui/react-dialog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Vite's &lt;code&gt;fs.allow&lt;/code&gt; narrows when you have multiple Astro apps
&lt;/h3&gt;

&lt;p&gt;After adding &lt;code&gt;linkpreview.ai&lt;/code&gt; as a fourth Astro app, running &lt;code&gt;pnpm --filter didof.dev dev&lt;/code&gt; started emitting this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[vite] The request url ".../node_modules/.pnpm/astro@.../dist/runtime/client/dev-toolbar/entrypoint.js" is outside of Vite serving allow list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vite's dev server has an allowlist of paths it's willing to serve files from. With one Astro app in a repo, it correctly detects the project root and includes the hoisted &lt;code&gt;node_modules/.pnpm/&lt;/code&gt; store two levels up. With multiple sibling Astro apps, the auto-detection gets narrower and excludes the store, which breaks Astro's own dev toolbar.&lt;/p&gt;

&lt;p&gt;Fix: explicitly tell Vite the whole monorepo is fair game, in each Astro app's config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fileURLToPath&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;node:url&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolve&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;node:path&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;MONOREPO_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fileURLToPath&lt;/span&gt;&lt;span class="p"&gt;(&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../..&lt;/span&gt;&lt;span class="dl"&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;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;vite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;MONOREPO_ROOT&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="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Native-binding packages need &lt;code&gt;approve-builds&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Packages like &lt;code&gt;sharp&lt;/code&gt;, &lt;code&gt;esbuild&lt;/code&gt;, and &lt;code&gt;@mediapipe/hands&lt;/code&gt; ship native binaries that install via postinstall scripts. pnpm 10+ blocks those scripts by default for security. The first time you try to use sharp, it errors with "postinstall was skipped."&lt;/p&gt;

&lt;p&gt;Fix: run &lt;code&gt;pnpm approve-builds&lt;/code&gt; once and pick the packages you trust. pnpm records them in your &lt;code&gt;.npmrc&lt;/code&gt; or &lt;code&gt;package.json&lt;/code&gt; so future installs run their postinstalls automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. &lt;code&gt;workspace:*&lt;/code&gt; does not work for dynamic &lt;code&gt;import()&lt;/code&gt; from Node scripts
&lt;/h3&gt;

&lt;p&gt;My build scripts do &lt;code&gt;await import("velocaption.com/tools-registry")&lt;/code&gt; at build time. With Vite handling the import, the workspace resolution works. With a plain Node &lt;code&gt;tsx&lt;/code&gt; script running outside Vite's resolution, it does not: Node's module resolver has no idea what &lt;code&gt;velocaption.com&lt;/code&gt; is.&lt;/p&gt;

&lt;p&gt;Fix for build scripts that need to reach into sibling packages: resolve absolute paths with &lt;code&gt;node:path&lt;/code&gt; and use &lt;code&gt;pathToFileURL&lt;/code&gt;:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolve&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;node:path&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathToFileURL&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;node:url&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;registryPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../velocaption.com/src/features/tools/registry.ts&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pathToFileURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;registryPath&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&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;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TOOL_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives up the package-name aliasing but works from any Node context. For scripts that only run through Vite (e.g. inside Astro pages), stick with &lt;code&gt;workspace:*&lt;/code&gt; imports.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Reach for Turborepo or Nx
&lt;/h2&gt;

&lt;p&gt;I don't use either, and I've been happy with plain pnpm workspaces for four apps. But there's a threshold.&lt;/p&gt;

&lt;p&gt;Turborepo earns its keep when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have so many packages that topological ordering alone isn't enough and you need smart caching of build outputs across runs&lt;/li&gt;
&lt;li&gt;CI needs to skip rebuilds of packages whose source files haven't changed&lt;/li&gt;
&lt;li&gt;You want a built-in dependency graph visualizer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nx earns its keep when you're in a strongly-typed polyglot monorepo (TypeScript plus Python plus Go) or when your team wants generators to scaffold new packages with the same shape.&lt;/p&gt;

&lt;p&gt;For "four websites plus a shared lib, all in TypeScript, built independently," pnpm workspaces is the correct tool. Don't let FOMO push you onto a bigger runner before you've felt the specific pain it solves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ERR_PNPM_NOTHING_TO_DEPLOY&lt;/code&gt; from your &lt;code&gt;ship&lt;/code&gt; script&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pnpm deploy&lt;/code&gt; is a built-in&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;pnpm run deploy&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pnpm add&lt;/code&gt; behaves oddly from the repo root&lt;/td&gt;
&lt;td&gt;Missing &lt;code&gt;-w&lt;/code&gt; flag&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pnpm add -D -w &amp;lt;pkg&amp;gt;&lt;/code&gt; to add to root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vite dev server warns about &lt;code&gt;fs.allow&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Multiple Astro apps narrow the allowlist&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;server.fs.allow: [monorepoRoot]&lt;/code&gt; to each Astro's Vite config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pnpm install&lt;/code&gt; logs &lt;code&gt;postinstall was skipped for sharp&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pnpm 10+ security default&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pnpm approve-builds&lt;/code&gt; once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sibling workspace import fails in a plain Node script&lt;/td&gt;
&lt;td&gt;Node module resolver can't see workspace: links&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pathToFileURL(resolve(__dirname, "..."))&lt;/code&gt; for dynamic import&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-package type imports not resolving&lt;/td&gt;
&lt;td&gt;Missing &lt;code&gt;exports&lt;/code&gt; map on the sibling's package.json&lt;/td&gt;
&lt;td&gt;Add subpath exports, reimport&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I use pnpm workspaces for a two-package repo?
&lt;/h3&gt;

&lt;p&gt;Yes, even for two. The overhead is a three-line YAML file. The payoff is a single lockfile, cross-package references via &lt;code&gt;workspace:*&lt;/code&gt;, and the ability to grow without restructuring. Two becomes four becomes seven, and you don't want to be migrating your dep model at that point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to publish &lt;code&gt;@didof/shared&lt;/code&gt; to npm for the workspace to work?
&lt;/h3&gt;

&lt;p&gt;No. Workspace dependencies resolve through local symlinks at install time. Publishing is only needed when consumers outside the workspace want to pull your shared library. All the code in this post runs entirely locally with no registry round-trip.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about CI? Does pnpm workspace break GitHub Actions?
&lt;/h3&gt;

&lt;p&gt;Not at all. The pnpm/action-setup GitHub Action handles workspaces out of the box. One &lt;code&gt;pnpm install&lt;/code&gt; at the root resolves the entire graph. Your CI steps become &lt;code&gt;pnpm --filter &amp;lt;package&amp;gt; build&lt;/code&gt; or &lt;code&gt;pnpm -r test&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I have different TypeScript versions per package?
&lt;/h3&gt;

&lt;p&gt;Technically yes, but I wouldn't. Put TypeScript at the root as a devDependency, let every package share the same version, and use per-package &lt;code&gt;tsconfig.json&lt;/code&gt; files with &lt;code&gt;extends&lt;/code&gt; to differentiate compile options. Version drift across packages creates confusing type errors that are hard to debug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does &lt;code&gt;workspace:*&lt;/code&gt; work with Yarn Berry or npm workspaces?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;workspace:*&lt;/code&gt; protocol originated in pnpm and is now supported by Yarn Berry and npm 7+. The filter commands differ: npm uses &lt;code&gt;--workspace=&amp;lt;name&amp;gt;&lt;/code&gt;, Yarn uses &lt;code&gt;yarn workspace &amp;lt;name&amp;gt; &amp;lt;cmd&amp;gt;&lt;/code&gt;, pnpm uses &lt;code&gt;--filter &amp;lt;name&amp;gt;&lt;/code&gt;. The underlying workspace resolution is similar across all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The switch cost me one afternoon of moving files and two additional hours fighting the five gotchas above. It's paid for itself every week since. Shared library edits propagate instantly. Cross-site data flows through direct filesystem reads. Installing dependencies takes seconds because everything's already in the store. Lockfile drift between sites is gone.&lt;/p&gt;

&lt;p&gt;If you have more than one related project sharing a codebase, there's probably a pnpm workspace in your future. Start with three lines of YAML.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>tutorial</category>
      <category>npm</category>
    </item>
    <item>
      <title>Remote Terminal: iPhone to Mac via Tailscale</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Sat, 11 Apr 2026 09:07:40 +0000</pubDate>
      <link>https://forem.com/didof/remote-terminal-iphone-to-mac-via-tailscale-3l6f</link>
      <guid>https://forem.com/didof/remote-terminal-iphone-to-mac-via-tailscale-3l6f</guid>
      <description>&lt;p&gt;I wanted to run terminal commands on my Mac from my iPhone. Not through some clunky VNC app. Not by opening SSH ports on my router. I wanted a real terminal, in my pocket, that works from anywhere, without exposing my machine to the internet.&lt;/p&gt;

&lt;p&gt;It took five minutes, two free tools, and zero networking knowledge. Here is exactly how.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tailscale creates a private WireGuard tunnel between your devices. No ports opened on your router.&lt;/li&gt;
&lt;li&gt;Remobi serves a web terminal via npx. One command, no install.&lt;/li&gt;
&lt;li&gt;Binding to your Tailscale IP (not &lt;code&gt;0.0.0.0&lt;/code&gt;) is the difference between "works" and "safe to leave running."&lt;/li&gt;
&lt;li&gt;Add it to your iPhone Home Screen and it behaves like a native app.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Are We Building?
&lt;/h2&gt;

&lt;p&gt;A full terminal running on your Mac, accessible from Safari on your iPhone, encrypted end-to-end, invisible to the public internet. You type commands on your phone, they execute on your Mac. You can be on the same WiFi or on the other side of the planet. It does not matter because the connection goes through a private mesh network, not the internet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://didof.dev/en/blog/remote-terminal-iphone-mac-tailscale/" rel="noopener noreferrer"&gt;Full blog post&lt;/a&gt; on my website.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The stack is two tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tailscale&lt;/strong&gt;: a free mesh VPN built on WireGuard. It gives each of your devices a private IP address (like &lt;code&gt;100.x.y.z&lt;/code&gt;) and routes traffic between them through an encrypted tunnel. Nothing is exposed to the public internet. No router configuration needed. If you have &lt;a href="https://dev.to/en/blog/setup-n8n-and-searxng-locally"&gt;local services like n8n running on your machine&lt;/a&gt;, Tailscale makes them accessible from your phone too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remobi&lt;/strong&gt;: a tiny terminal server you launch with &lt;code&gt;npx&lt;/code&gt;. It opens a web-based terminal that you access from any browser. No installation, no accounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What You Will Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Mac (any recent macOS version)&lt;/li&gt;
&lt;li&gt;An iPhone (or iPad, or any device with a browser)&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://tailscale.com/" rel="noopener noreferrer"&gt;Tailscale account&lt;/a&gt; (free for personal use, up to 100 devices)&lt;/li&gt;
&lt;li&gt;Node.js installed on your Mac (for &lt;code&gt;npx&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total setup time: about five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set Up Tailscale on Both Devices
&lt;/h2&gt;

&lt;p&gt;If you already use Tailscale, skip to the next section.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;a href="https://tailscale.com/download/mac" rel="noopener noreferrer"&gt;Tailscale on your Mac&lt;/a&gt; from the website or the App Store.&lt;/li&gt;
&lt;li&gt;Install &lt;a href="https://apps.apple.com/app/tailscale/id1470499037" rel="noopener noreferrer"&gt;Tailscale on your iPhone&lt;/a&gt; from the App Store.&lt;/li&gt;
&lt;li&gt;Log in with the same account on both devices.&lt;/li&gt;
&lt;li&gt;On your iPhone, toggle the VPN on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is it. Both devices are now on the same private network. You can verify by opening Terminal on your Mac and running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see both your Mac and your iPhone listed. Note your Mac's Tailscale IP. It starts with &lt;code&gt;100.&lt;/code&gt; and looks something like &lt;code&gt;100.122.133.96&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the &lt;code&gt;tailscale&lt;/code&gt; command is not found, add an alias to your shell config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'alias tailscale="/Applications/Tailscale.app/Contents/MacOS/Tailscale"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Launch the Terminal Server
&lt;/h2&gt;

&lt;p&gt;Here is where most guides get the security wrong.&lt;/p&gt;

&lt;p&gt;The obvious command to start Remobi is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx remobi@latest serve &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works. But &lt;code&gt;0.0.0.0&lt;/code&gt; tells your Mac to listen on every network interface. Your Tailscale tunnel, yes, but also your local WiFi. If you are at a coffee shop, anyone on the same network could theoretically browse to your local IP and get a terminal on your machine.&lt;/p&gt;

&lt;p&gt;The fix is simple. Bind Remobi to your Tailscale IP only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx remobi@latest serve &lt;span class="nt"&gt;--host&lt;/span&gt; 100.122.133.96
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace that IP with your actual Tailscale IP from the previous step. Now the terminal server only accepts connections from the private Tailscale network. Someone sitting next to you at Starbucks would not even see it.&lt;/p&gt;

&lt;p&gt;Keep this terminal window open. The server runs as long as the process is alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect From Your iPhone
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Make sure Tailscale is active on your iPhone (the VPN toggle in Settings or the Tailscale app).&lt;/li&gt;
&lt;li&gt;Open Safari and go to: &lt;code&gt;http://100.122.133.96:7681&lt;/code&gt; (using your Mac's Tailscale IP).&lt;/li&gt;
&lt;li&gt;You should see a full terminal. Try &lt;code&gt;ls&lt;/code&gt; or &lt;code&gt;pwd&lt;/code&gt; to confirm you are on your Mac.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now the trick that makes this feel native: tap the &lt;strong&gt;Share&lt;/strong&gt; button in Safari, then tap &lt;strong&gt;"Add to Home Screen."&lt;/strong&gt; Give it a name like "Mac Terminal." This creates a PWA-style shortcut that opens in full screen, no browser chrome. It looks and feels like an app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mirror the Session With tmux
&lt;/h2&gt;

&lt;p&gt;Remobi runs inside a tmux session called &lt;code&gt;main&lt;/code&gt;. This means you can attach to the same session from your Mac and see exactly what your iPhone is doing in real time.&lt;/p&gt;

&lt;p&gt;Open a new terminal tab on your Mac and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux attach &lt;span class="nt"&gt;-t&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both screens now show the same session. Type on your phone, it appears on your Mac. Type on your Mac, it appears on your phone. This is useful for debugging: run something from your phone while watching the output on a bigger screen.&lt;/p&gt;

&lt;p&gt;On my home WiFi, keystrokes appear instantly. Over LTE from a cafe, I noticed maybe a half-second delay, but for running git commands or restarting services it never mattered.&lt;/p&gt;

&lt;p&gt;To detach without killing the session, press &lt;code&gt;Ctrl+B&lt;/code&gt; then &lt;code&gt;D&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Your Mac Awake
&lt;/h2&gt;

&lt;p&gt;If your Mac goes to sleep, the Tailscale connection drops and Remobi dies. If you want to leave this running while you are away, you need to prevent sleep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: System Settings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;System Settings &amp;gt; Displays &amp;gt; Advanced&lt;/strong&gt; and enable &lt;strong&gt;"Prevent automatic sleeping when the display is off."&lt;/strong&gt; Also check &lt;strong&gt;System Settings &amp;gt; Battery &amp;gt; Options&lt;/strong&gt; and enable &lt;strong&gt;"Wake for network access."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: The caffeinate command&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run this in a separate terminal tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;caffeinate &lt;span class="nt"&gt;-s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the Mac awake as long as the command is running. Kill it with &lt;code&gt;Ctrl+C&lt;/code&gt; when you are done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3: Amphetamine&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apps.apple.com/app/amphetamine/id937984704" rel="noopener noreferrer"&gt;Amphetamine&lt;/a&gt; is a free app from the App Store that gives you fine-grained control over when your Mac stays awake. You can set it to stay awake only while Remobi is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Secure Is This Actually?
&lt;/h2&gt;

&lt;p&gt;Here is the honest breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Random attacker from the internet&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;None&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No ports are open on your router. There is nothing to find.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Someone on the same WiFi&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;None&lt;/strong&gt; (if you bound to Tailscale IP)&lt;/td&gt;
&lt;td&gt;The server only listens on the private Tailscale interface.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Someone on the same WiFi&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Medium&lt;/strong&gt; (if you used &lt;code&gt;0.0.0.0&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;They could find your local IP and access the terminal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailscale account compromise&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Extremely low&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An attacker would need to steal your Tailscale identity.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key takeaway: &lt;strong&gt;bind to your Tailscale IP, not &lt;code&gt;0.0.0.0&lt;/code&gt;.&lt;/strong&gt; That single flag is the difference between a locked door and an open one.&lt;/p&gt;

&lt;p&gt;All traffic between your iPhone and Mac is encrypted with &lt;a href="https://www.wireguard.com/" rel="noopener noreferrer"&gt;WireGuard&lt;/a&gt;, a modern VPN protocol that has been &lt;a href="https://www.wireguard.com/formal-verification/" rel="noopener noreferrer"&gt;formally verified&lt;/a&gt; and adopted by providers like Mullvad, Mozilla VPN, and Cloudflare WARP. Tailscale adds identity-based access control on top. Their &lt;a href="https://tailscale.com/security/" rel="noopener noreferrer"&gt;security model&lt;/a&gt; is well documented.&lt;/p&gt;

&lt;p&gt;When you are done, type &lt;code&gt;exit&lt;/code&gt; on your phone to close the terminal session, then &lt;code&gt;Ctrl+C&lt;/code&gt; on your Mac to stop the Remobi server.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Works on Linux and Windows Too
&lt;/h2&gt;

&lt;p&gt;Everything in this guide except the sleep prevention steps is OS-agnostic. Tailscale runs on Linux, Windows, and even Raspberry Pi. Remobi is just Node.js.&lt;/p&gt;

&lt;p&gt;On Linux, replace the sleep prevention with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-inhibit &lt;span class="nt"&gt;--what&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;idle npx remobi@latest serve &lt;span class="nt"&gt;--host&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;tailscale ip &lt;span class="nt"&gt;-4&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows, install Tailscale from their website, run &lt;code&gt;npx remobi@latest serve --host &amp;lt;your-tailscale-ip&amp;gt;&lt;/code&gt; in PowerShell, and connect from your phone the same way.&lt;/p&gt;

&lt;p&gt;The Tailscale IP binding trick works identically on every platform. The private network does not care what OS either end is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this work over mobile data or only on WiFi?
&lt;/h3&gt;

&lt;p&gt;It works everywhere. Tailscale routes traffic through relay servers (called DERP) when a direct connection is not possible. You can be on cellular data on your iPhone and WiFi on your Mac. The connection goes through Tailscale's infrastructure, encrypted end-to-end. Latency is slightly higher over cellular, but for terminal use it is unnoticeable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use this with an Android phone?
&lt;/h3&gt;

&lt;p&gt;Yes. Tailscale has an &lt;a href="https://play.google.com/store/apps/details?id=com.tailscale.ipn" rel="noopener noreferrer"&gt;Android app&lt;/a&gt;. The browser-based terminal works in Chrome on Android the same way it works in Safari on iPhone. You can also add it to your home screen as a PWA.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Remobi safe to use? It runs via npx.
&lt;/h3&gt;

&lt;p&gt;Remobi is open source (&lt;a href="https://github.com/connorads/remobi" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;) and small enough to audit in an afternoon. It wraps &lt;a href="https://github.com/nickoala/nickoala-ttyd" rel="noopener noreferrer"&gt;ttyd&lt;/a&gt;, a well-known web terminal tool. Running it via npx means you always get the latest version. The security boundary is Tailscale, not Remobi: even if Remobi had a vulnerability, only devices on your Tailnet can reach it.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How K-Means Clustering Works (Explained by Extracting Colors from Images)</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Thu, 09 Apr 2026 08:03:00 +0000</pubDate>
      <link>https://forem.com/didof/how-k-means-clustering-works-explained-by-extracting-colors-from-images-pmi</link>
      <guid>https://forem.com/didof/how-k-means-clustering-works-explained-by-extracting-colors-from-images-pmi</guid>
      <description>&lt;p&gt;Every color palette extractor you've used runs K-Means clustering. Coolors, Canva, Adobe. They all use the same algorithm to find dominant colors. Most of them run it on a server. Ours runs it on your GPU, entirely in the browser.&lt;/p&gt;

&lt;p&gt;We built a &lt;a href="https://velocaption.com/tools/palette-extractor" rel="noopener noreferrer"&gt;free Palette Extractor&lt;/a&gt; that processes 65,536 pixels in 12ms using a WebGL2 fragment shader. This post walks through how K-Means works, why color extraction is the perfect demo, and how 15 lines of GLSL turn the GPU into a parallel K-Means engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  K-Means in 30 Seconds
&lt;/h2&gt;

&lt;p&gt;The algorithm repeats two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assign&lt;/strong&gt; every data point to the nearest centroid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update&lt;/strong&gt; each centroid to the mean of its assigned points&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Loop until nothing changes. For color extraction, data points are pixels (RGB values) and centroids become your palette colors.&lt;/p&gt;

&lt;p&gt;It's the most widely used clustering algorithm in ML (&lt;a href="https://www.ibm.com/think/topics/k-means-clustering" rel="noopener noreferrer"&gt;IBM&lt;/a&gt;), and it scales at O(n).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Steps (With Colors)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Choose K&lt;/strong&gt;: How many palette colors do you want? K=6 means 6 clusters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialize&lt;/strong&gt;: Pick K random pixels as starting centroids. Wrong guesses are fine. The algorithm fixes them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assign&lt;/strong&gt;: Every pixel calculates Euclidean distance to every centroid in RGB space. Snaps to the nearest one. For a 256x256 image, that's 65,536 pixels x K centroids = a lot of distance calculations. Per iteration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Each centroid moves to the average color of its assigned pixels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Converge&lt;/strong&gt;: Repeat assign+update until centroids stop moving. Typical images converge in 5-15 iterations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the GPU?
&lt;/h2&gt;

&lt;p&gt;The assignment step is embarrassingly parallel. Each pixel's computation is independent. No pixel needs to know about any other pixel. That's exactly what GPUs do: run thousands of identical programs simultaneously.&lt;/p&gt;

&lt;p&gt;When we first tried running K-Means in JavaScript on the CPU, anything above 256x256 was noticeably slow. Moving the assignment step to a WebGL2 fragment shader made it instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fragment Shader
&lt;/h2&gt;

&lt;p&gt;This is the actual production code. Not pseudocode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="cp"&gt;#version 300 es
&lt;/span&gt;&lt;span class="k"&gt;precision&lt;/span&gt; &lt;span class="kt"&gt;highp&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;uniform&lt;/span&gt; &lt;span class="kt"&gt;sampler2D&lt;/span&gt; &lt;span class="n"&gt;u_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// pixel colors as texture&lt;/span&gt;
&lt;span class="k"&gt;uniform&lt;/span&gt; &lt;span class="kt"&gt;vec3&lt;/span&gt; &lt;span class="n"&gt;u_centroids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;   &lt;span class="c1"&gt;// current centroid positions&lt;/span&gt;
&lt;span class="k"&gt;uniform&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;u_k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                &lt;span class="c1"&gt;// number of clusters&lt;/span&gt;
&lt;span class="k"&gt;uniform&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;u_numPoints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// total pixel count&lt;/span&gt;
&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt; &lt;span class="n"&gt;outColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;ivec2&lt;/span&gt; &lt;span class="n"&gt;texCoord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;ivec2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;gl_FragCoord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;texWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textureSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;texCoord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;texWidth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;texCoord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&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="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;u_numPoints&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;outColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt;&lt;span class="p"&gt;(&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="mi"&gt;0&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;vec3&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;texelFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;texCoord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;minDist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;999999&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;bestK&lt;/span&gt; &lt;span class="o"&gt;=&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="mi"&gt;0&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&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="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;u_k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u_centroids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;minDist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;minDist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;bestK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&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="n"&gt;outColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bestK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&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 trick: pixel RGB values are packed into an RGBA32F floating-point texture. The shader runs once per texel, computes distance to each centroid, and outputs the cluster assignment. One &lt;code&gt;drawArrays&lt;/code&gt; call processes all 65,536 pixels in parallel.&lt;/p&gt;

&lt;p&gt;GPU K-Means achieves 100x+ speedup over CPU at scale (&lt;a href="https://link.springer.com/article/10.1007/s10618-022-00869-6" rel="noopener noreferrer"&gt;Springer, 2022&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hybrid Loop
&lt;/h2&gt;

&lt;p&gt;The full K-Means loop isn't entirely on the GPU. Assignment parallelizes perfectly, but centroid update requires aggregation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CPU&lt;/strong&gt; uploads centroid positions as shader uniforms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU&lt;/strong&gt; runs the fragment shader (assignment)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU&lt;/strong&gt; reads results via &lt;code&gt;readPixels&lt;/code&gt; (the bottleneck)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU&lt;/strong&gt; accumulates sums per cluster, divides to get new centroids&lt;/li&gt;
&lt;li&gt;Repeat until converged&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;readPixels&lt;/code&gt; stalls the pipeline (GPU-to-CPU transfer), but the parallel assignment step more than compensates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real Numbers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;256x256 image, K=6&lt;/strong&gt;: 5-8 iterations, 8-15ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;256x256 image, K=12&lt;/strong&gt;: 8-12 iterations, 15-25ms&lt;/li&gt;
&lt;li&gt;Max 50 iterations cap. Never seen an image need more than 20.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://velocaption.com/tools/palette-extractor" rel="noopener noreferrer"&gt;Palette Extractor&lt;/a&gt; is free, runs in any browser with WebGL2 (92% support), and never uploads your images. Drop an image, pick K, get colors.&lt;/p&gt;

&lt;p&gt;Full writeup with the interactive K-Means step-by-step visualizer: &lt;a href="https://velocaption.com/blog/k-means-gpu-palette-extractor" rel="noopener noreferrer"&gt;velocaption.com/blog/k-means-gpu-palette-extractor&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We build browser-based tools at &lt;a href="https://velocaption.com" rel="noopener noreferrer"&gt;Velocaption&lt;/a&gt;. Our main product is a desktop video editor for silence removal and auto-captioning. If you're curious about browser performance, we also wrote about &lt;a href="https://velocaption.com/blog/media-pool-smooth-playback" rel="noopener noreferrer"&gt;how we got 60fps video playback&lt;/a&gt; using a similar bypass-the-framework approach.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webgl</category>
      <category>machinelearning</category>
      <category>javascript</category>
      <category>gpu</category>
    </item>
    <item>
      <title>JSON-LD Structured Data for Blogs: A Real Implementation</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Wed, 08 Apr 2026 07:40:00 +0000</pubDate>
      <link>https://forem.com/didof/json-ld-structured-data-for-blogs-a-real-implementation-25n</link>
      <guid>https://forem.com/didof/json-ld-structured-data-for-blogs-a-real-implementation-25n</guid>
      <description>&lt;p&gt;Rich results achieve &lt;strong&gt;82% higher click-through rates&lt;/strong&gt; compared to standard search listings (&lt;a href="https://www.searchenginejournal.com/structured-data-rich-results/" rel="noopener noreferrer"&gt;Search Engine Journal, 2025&lt;/a&gt;). But in 2026, structured data is no longer just about rich snippets. It is how AI systems like Google AI Overviews, ChatGPT, and Perplexity decide whether your content is worth citing. According to &lt;a href="https://www.semrush.com/blog/semrush-ai-overviews-study/" rel="noopener noreferrer"&gt;Semrush's 2025 AI Overviews study&lt;/a&gt;, AI Overviews now appear in over 15% of Google searches, and &lt;strong&gt;pages with clear semantic structure are significantly more likely to be cited&lt;/strong&gt; in those results.&lt;/p&gt;

&lt;p&gt;I just added JSON-LD structured data to this very blog. In this post, I will walk through the exact code, the architectural decisions, and the verification steps. No toy examples, just production-ready implementation. If you care about &lt;a href="https://velocaption.com/en/blog/cache-control-max-age-stale-while-revalidate" rel="noopener noreferrer"&gt;HTTP caching headers&lt;/a&gt; or &lt;a href="https://velocaption.com/en/blog/etag-if-none-match" rel="noopener noreferrer"&gt;ETags for content freshness&lt;/a&gt;, you already understand the value of giving browsers and crawlers explicit signals. JSON-LD structured data does the same thing for search engines and AI systems.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post is better to &lt;a href="https://didof.dev/en/blog/json-ld-structured-data/" rel="noopener noreferrer"&gt;read on my website&lt;/a&gt; :)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is JSON-LD and Why Does Google Recommend It?
&lt;/h2&gt;

&lt;p&gt;JSON-LD (JavaScript Object Notation for Linked Data) is a method of encoding structured data using JSON. Google explicitly states in their &lt;a href="https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data" rel="noopener noreferrer"&gt;developer documentation&lt;/a&gt;: &lt;strong&gt;"Google recommends using JSON-LD for structured data whenever possible."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are three formats for structured data on the web:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microdata&lt;/strong&gt;: attributes woven into your HTML elements. Tightly coupled to your markup, hard to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDFa&lt;/strong&gt;: similar to Microdata but more verbose. Used by some CMS platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON-LD&lt;/strong&gt;: a standalone &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block in your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. Completely decoupled from your HTML.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JSON-LD wins because it lives separately from your content. You can add, remove, or modify your structured data without touching a single line of your page's HTML. For component-based frameworks like Astro, React, or Next.js, this is the natural choice. Your schema is just another data object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&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;@context&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;https://schema.org&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;@type&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;BlogPosting&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;headline&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;Your Article Title&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;datePublished&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;2026-04-07T00:00:00.000Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Which BlogPosting Fields Actually Matter?
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://schema.org/BlogPosting" rel="noopener noreferrer"&gt;schema.org BlogPosting type&lt;/a&gt; has dozens of properties, but not all of them are equal. Here is what Google actually uses for rich results and what AI systems look for when deciding whether to cite you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Required for rich results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;headline&lt;/code&gt;: your article title&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;datePublished&lt;/code&gt;: ISO 8601 format&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;author&lt;/code&gt;: a &lt;code&gt;Person&lt;/code&gt; or &lt;code&gt;Organization&lt;/code&gt; with at least a &lt;code&gt;name&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Strongly recommended:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dateModified&lt;/code&gt;: signals freshness. 76% of top AI-cited content was updated within 30 days.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;description&lt;/code&gt;: helps AI systems understand the article's scope&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;image&lt;/code&gt;: enables visual rich results&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;url&lt;/code&gt;: canonical URL of the article&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Good to have:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;publisher&lt;/code&gt;: for E-E-A-T signals. Can be the same as &lt;code&gt;author&lt;/code&gt; for personal blogs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not over-engineer it. A clean schema with the right fields beats a bloated one with properties Google ignores.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Build a JSON-LD Structured Data Component in Astro?
&lt;/h2&gt;

&lt;p&gt;Here is the actual component I built for this blog. It lives at &lt;code&gt;src/components/seo/json-ld.astro&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
export interface Props {
  title: "string;"
  description: "string;"
  url: string;
  image?: string;
  datePublished: string;
  dateModified?: string;
  author?: string;
}

const {
  title,
  description,
  url,
  image,
  datePublished,
  dateModified,
  author = "Francesco Di Donato",
} = Astro.props;

const schema = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  headline: title,
  description,
  url,
  ...(image &amp;amp;&amp;amp; { image }),
  datePublished,
  ...(dateModified &amp;amp;&amp;amp; { dateModified }),
  author: {
    "@type": "Person",
    name: author,
    url: "https://didof.dev",
  },
  publisher: {
    "@type": "Person",
    name: author,
    url: "https://didof.dev",
  },
};
---

&amp;lt;script type="application/ld+json" set:html={JSON.stringify(schema)} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth noting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conditional spreading&lt;/strong&gt;: &lt;code&gt;...(image &amp;amp;&amp;amp; { image })&lt;/code&gt; only includes the &lt;code&gt;image&lt;/code&gt; field when one exists. Same for &lt;code&gt;dateModified&lt;/code&gt;. This avoids &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt; values in your JSON-LD, which can trigger validation warnings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The &lt;code&gt;set:html&lt;/code&gt; directive&lt;/strong&gt;: Astro's &lt;code&gt;set:html&lt;/code&gt; safely injects the JSON string into the script tag. Never use template literals for this. &lt;code&gt;set:html={JSON.stringify(schema)}&lt;/code&gt; handles escaping correctly and prevents XSS vectors if any prop contains user-controlled data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Typed Props&lt;/strong&gt;: the &lt;code&gt;Props&lt;/code&gt; interface ensures TypeScript catches missing required fields at build time, not in production.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How Do You Wire JSON-LD Into Your Layout Conditionally?
&lt;/h2&gt;

&lt;p&gt;The key architectural decision: JSON-LD should only render on blog posts, not on every page. The homepage, about page, and tools pages do not need &lt;code&gt;BlogPosting&lt;/code&gt; schema.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;src/layouts/main.astro&lt;/code&gt;, I added an optional &lt;code&gt;schema&lt;/code&gt; prop:&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="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&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;description&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;image&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;footer&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;header&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;disableFloatingCal&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;datePublished&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;dateModified&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, the component renders conditionally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{schema &amp;amp;&amp;amp; (
  &amp;lt;BlogJsonLd
    title={title || "didof.dev"}
    description={description}
    url={canonicalURL.toString()}
    image={new URL(image, Astro.site).toString()}
    datePublished={schema.datePublished}
    dateModified={schema.dateModified}
  /&amp;gt;
)}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blog post page (&lt;code&gt;src/pages/[locale]/blog/[...slug].astro&lt;/code&gt;) passes the schema data from the content collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Main
  title={post.data.title}
  description={post.data.description}
  image={ogImage?.src}
  schema={{
    datePublished: post.data.date.toISOString(),
    dateModified: post.data.update_date?.toISOString(),
  }}
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every blog post now gets structured data automatically. No manual JSON-LD per post, no forgetting to add it. If &lt;code&gt;update_date&lt;/code&gt; is not set in the frontmatter, &lt;code&gt;dateModified&lt;/code&gt; simply does not appear in the schema. Exactly the behavior we want.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Verify JSON-LD Structured Data Is Working?
&lt;/h2&gt;

&lt;p&gt;After building, you can verify JSON-LD is present in the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check that blog posts contain JSON-LD&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"application/ld+json"&lt;/span&gt; dist/en/blog/&lt;span class="k"&gt;*&lt;/span&gt;/index.html | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;

&lt;span class="c"&gt;# Inspect the actual schema for one post&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;script type="application/ld+json"&amp;gt;[^&amp;lt;]*&amp;lt;/script&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  dist/en/blog/setup-n8n-and-searxng-locally/index.html  &lt;span class="c"&gt;# any post works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output should be valid JSON with &lt;code&gt;@type: "BlogPosting"&lt;/code&gt;, your &lt;code&gt;headline&lt;/code&gt;, &lt;code&gt;datePublished&lt;/code&gt;, and &lt;code&gt;author&lt;/code&gt; fields.&lt;/p&gt;

&lt;p&gt;For production validation, use two tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://validator.schema.org/" rel="noopener noreferrer"&gt;Schema Markup Validator&lt;/a&gt;&lt;/strong&gt;: paste your URL to check for schema.org compliance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://search.google.com/test/rich-results" rel="noopener noreferrer"&gt;Google Rich Results Test&lt;/a&gt;&lt;/strong&gt;: shows what Google can extract and whether you qualify for rich results&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Does JSON-LD Structured Data Matter for AI Search in 2026?
&lt;/h2&gt;

&lt;p&gt;Structured data is no longer optional SEO polish. It is infrastructure for AI visibility.&lt;/p&gt;

&lt;p&gt;When Google AI Overviews, ChatGPT web search, or Perplexity crawl your page, they are trying to answer three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What is this content about?&lt;/strong&gt; &lt;code&gt;headline&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;@type&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it trustworthy?&lt;/strong&gt; &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;publisher&lt;/code&gt;, &lt;code&gt;datePublished&lt;/code&gt;, &lt;code&gt;dateModified&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it current?&lt;/strong&gt; &lt;code&gt;dateModified&lt;/code&gt; is the strongest freshness signal&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without structured data, AI systems have to guess these answers from your HTML. With it, you are handing them the answers directly, in a format they are designed to consume.&lt;/p&gt;

&lt;p&gt;JSON-LD structured data can increase your click-through rate by &lt;strong&gt;35%&lt;/strong&gt; even without a visual rich result, because search engines understand your content better and rank it more confidently. If you are building tools that interact with APIs, like &lt;a href="https://dev.to/en/blog/setup-n8n-and-searxng-locally"&gt;setting up n8n and SearXNG locally&lt;/a&gt;, adding structured data helps those pages get discovered by the same AI systems you are building with.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need a separate JSON-LD block for each blog post?
&lt;/h3&gt;

&lt;p&gt;No. With the component approach shown above, every blog post automatically gets its own JSON-LD block generated from the content collection data. You write the component once and it scales to every post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does JSON-LD affect page performance?
&lt;/h3&gt;

&lt;p&gt;No measurable impact. The JSON-LD script block is a few hundred bytes of text. It does not execute as JavaScript. The browser ignores &lt;code&gt;type="application/ld+json"&lt;/code&gt; scripts entirely. Only search engines and AI crawlers parse them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use the &lt;code&gt;astro-seo-schema&lt;/code&gt; npm package instead?
&lt;/h3&gt;

&lt;p&gt;It depends. The &lt;code&gt;astro-seo-schema&lt;/code&gt; package provides TypeScript definitions from &lt;code&gt;schema-dts&lt;/code&gt;, which gives you autocomplete for every schema.org property. For a simple &lt;code&gt;BlogPosting&lt;/code&gt; schema, a hand-rolled component like the one above is lighter and gives you full control. For complex schemas with multiple nested types, the package can help prevent mistakes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about FAQ schema on this very page?
&lt;/h3&gt;

&lt;p&gt;Google deprecated &lt;code&gt;HowTo&lt;/code&gt; rich results in September 2023, but &lt;code&gt;FAQPage&lt;/code&gt; schema remains supported. Google now shows FAQ rich results less frequently though. The value of FAQ sections in 2026 is primarily for AI citation: question-answer formats are the easiest structure for AI systems to extract and cite.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>html</category>
      <category>seo</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Use a Reverse Trie for Fast Disposable Email Domain Detection</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Fri, 06 Dec 2024 11:25:36 +0000</pubDate>
      <link>https://forem.com/didof/how-to-use-a-reverse-trie-for-fast-disposable-email-domain-detection-2o50</link>
      <guid>https://forem.com/didof/how-to-use-a-reverse-trie-for-fast-disposable-email-domain-detection-2o50</guid>
      <description>&lt;p&gt;Learn how to use a reverse Trie to efficiently detect disposable email domains. Optimize your domain lookups with a scalable, memory-efficient solution tailored for fast and precise results.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the article on &lt;a href="https://didof.dev/blog/authentication/reverse-trie-for-fast-disposable-email-domain-detection" rel="noopener noreferrer"&gt;my website&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Use the &lt;a href="https://didof.dev/free-tools/disposable-email-domain-detector" rel="noopener noreferrer"&gt;Free Disposable Email Domain Detector&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Disposable emails can cause issues like fake signups and spam. The user grabs an address from one of thousands of temporary email generators and hands it over. Not even the &lt;a href="https://emailregex.com/" rel="noopener noreferrer"&gt;GOAT of email regex&lt;/a&gt; can save you here.&lt;/p&gt;

&lt;p&gt;Personally, I find having a big list of all disposable email domains is the easiest yet most effective solution. But before you assemble that list and start a for ... of loop to check against it, think of the O(n) complexity!&lt;/p&gt;

&lt;p&gt;A great way to identify them is by using a &lt;strong&gt;reverse Trie&lt;/strong&gt;, an efficient data structure for fast lookups.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Reverse Trie?
&lt;/h2&gt;

&lt;p&gt;First, let's grasp what a Trie is. It is a data structure where strings are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;chopped up, char per char&lt;/li&gt;
&lt;li&gt;assembled in a tree structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;example, if we feed &lt;code&gt;boa&lt;/code&gt;, &lt;code&gt;bro&lt;/code&gt;, &lt;code&gt;brie&lt;/code&gt;, it would assemble them using Map as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;b&lt;/span&gt;
 &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nt"&gt;o&lt;/span&gt; &lt;span class="err"&gt;──&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt;
 &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nt"&gt;r&lt;/span&gt; &lt;span class="err"&gt;──&lt;/span&gt; &lt;span class="nt"&gt;o&lt;/span&gt;  
     &lt;span class="err"&gt;└───&lt;/span&gt; &lt;span class="nt"&gt;i&lt;/span&gt; &lt;span class="err"&gt;──&lt;/span&gt; &lt;span class="nt"&gt;e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach allows direct lookups without cycling through the entire list. Each character guides the search deeper.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It trades memory for efficiency&lt;/strong&gt;. The time it takes to find the string does not depend on the size of the list, but on the length of the string!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A &lt;strong&gt;reverse Trie&lt;/strong&gt; stores strings in reverse order, ideal for domains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mailinator.com&lt;/code&gt; becomes &lt;code&gt;moc.rotanliam&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trashmail.com&lt;/code&gt; becomes &lt;code&gt;moc.liambhsart&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Note on This Implementatin
&lt;/h3&gt;

&lt;p&gt;By reversing domains, searches start at the &lt;a href="https://en.wikipedia.org/wiki/Top-level_domain" rel="noopener noreferrer"&gt;TLD&lt;/a&gt; (e.g., &lt;code&gt;.com&lt;/code&gt;), which is shared across many domains. To optimize further, it stores &lt;code&gt;TLD&lt;/code&gt;s as a single key (&lt;code&gt;com&lt;/code&gt;), rather than splitting them into characters. The rest of the domain follows a standard Trie structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reverse Trie Domains Implementation
&lt;/h2&gt;

&lt;p&gt;Since this is a tree structure, each node will reference its children:&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;type&lt;/span&gt; &lt;span class="nx"&gt;TrieNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Map&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="nx"&gt;TrieNode&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, a utility function to split the TLD from the rest of the domain:&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="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;splitTLDFromRest&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="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;dot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&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;TLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dot&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;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&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;dot&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="nx"&gt;TLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rest&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;blockquote&gt;
&lt;p&gt;Using &lt;code&gt;lastIndexOf&lt;/code&gt; ensures subdomains like &lt;code&gt;foo.bar.baz.com&lt;/code&gt; are handled correctly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, the constructor will assemble the Trie:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReverseTrieDomains&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrieNode&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;domains&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="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;domain&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// For "didof.dev"&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;TLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitTLDFromRest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// dev, didof&lt;/span&gt;

            &lt;span class="c1"&gt;// Keep the refence to the TLD node for final set&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TLD&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;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;node&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Start from TLD node, walk along the string in reverse&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrieNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&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="nx"&gt;rest&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="mi"&gt;1&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;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;--&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;char&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&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;let&lt;/span&gt; &lt;span class="nx"&gt;childNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&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;childNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;childNode&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;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                    &lt;span class="nx"&gt;currentNode&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;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;childNode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nx"&gt;currentNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;childNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&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;TLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check if a domain is disposable, traverse the Trie:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReverseTrieDomains&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;TLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitTLDFromRest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&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;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TLD&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;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrieNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isFullDomainFound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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="nx"&gt;rest&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="mi"&gt;1&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;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;--&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;char&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&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;childNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&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;childNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
            &lt;span class="nx"&gt;currentNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;childNode&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;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="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;isFullDomainFound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&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="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="nx"&gt;isFullDomainFound&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;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Using a reverse Trie offers several benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast Lookups&lt;/strong&gt;: Traverse characters step-by-step for quick results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory Efficiency&lt;/strong&gt;: Common suffixes like &lt;code&gt;.com&lt;/code&gt; are stored only once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Handles large domain lists effortlessly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re dealing with disposable emails, this is a smart, scalable solution to implement.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Throttling Explained: A Guide to Managing API Request Limits</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Wed, 04 Dec 2024 18:15:00 +0000</pubDate>
      <link>https://forem.com/didof/throttling-explained-a-guide-to-managing-api-request-limits-102a</link>
      <guid>https://forem.com/didof/throttling-explained-a-guide-to-managing-api-request-limits-102a</guid>
      <description>&lt;h2&gt;
  
  
  When Should You Implement Throttling in Your Code?
&lt;/h2&gt;

&lt;p&gt;For big projects, it’s usually best to use tools like &lt;a href="https://www.cloudflare.com/en-gb/application-services/products/rate-limiting/" rel="noopener noreferrer"&gt;Cloudflare Rate Limiting&lt;/a&gt; or &lt;a href="https://www.haproxy.com/documentation/haproxy-configuration-tutorials/traffic-policing/" rel="noopener noreferrer"&gt;HAProxy&lt;/a&gt;. These are powerful, reliable, and take care of the heavy lifting for you.&lt;/p&gt;

&lt;p&gt;But for smaller projects—or if you want to learn how things work—you can create your own rate limiter right in your code. Why?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It’s Simple&lt;/strong&gt;: You’ll build something straightforward that’s easy to understand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It’s Budget-Friendly&lt;/strong&gt;: No extra costs beyond hosting your server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It Works for Small Projects&lt;/strong&gt;: As long as traffic is low, it keeps things fast and efficient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It’s Reusable&lt;/strong&gt;: You can copy it into other projects without setting up new tools or services.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What You Will Learn
&lt;/h2&gt;

&lt;p&gt;By the end of this guide, you’ll know how to build a basic throttler in &lt;a href="https://www.typescriptlang.org/" rel="noopener noreferrer"&gt;TypeScript&lt;/a&gt; to protect your APIs from being overwhelmed. Here’s what we’ll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configurable Time Limits&lt;/strong&gt;: Each blocked attempt increases the lockout duration to prevent abuse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request Caps&lt;/strong&gt;: Set a maximum number of allowed requests. This is especially useful for APIs that involve paid services, like OpenAI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-Memory Storage&lt;/strong&gt;: A simple solution that works without external tools like Redis—ideal for small projects or prototypes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-User Limits&lt;/strong&gt;: Track requests on a per-user basis using their IPv4 address. We’ll leverage &lt;a href="https://svelte.dev/docs/kit/@sveltejs-kit#RequestEvent" rel="noopener noreferrer"&gt;SvelteKit&lt;/a&gt; to easily retrieve the client IP with its &lt;a href="https://github.com/sveltejs/kit/pull/4289" rel="noopener noreferrer"&gt;built-in method&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;This guide is designed to be a practical starting point, perfect for developers who want to learn the basics without unnecessary complexity. &lt;strong&gt;But it is not production-ready&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before starting, I want to give the right credits to &lt;a href="https://lucia-next.pages.dev/rate-limit/throttling" rel="noopener noreferrer"&gt;Lucia's Rate Limiting section&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Throttler Implementation
&lt;/h2&gt;

&lt;p&gt;Let’s define the &lt;code&gt;Throttler&lt;/code&gt; class:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Throttler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&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="nx"&gt;ThrottlingCounter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;The &lt;code&gt;Throttler&lt;/code&gt; constructor accepts a list of timeout durations (&lt;code&gt;timeoutSeconds&lt;/code&gt;). Each time a user is blocked, the duration increases progressively based on this list. Eventually, when the final timeout is reached, you could even trigger a callback to permanently ban the user’s IP—though that’s beyond the scope of this guide.&lt;/p&gt;

&lt;p&gt;Here’s an example of creating a throttler instance that blocks users for increasing intervals:&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;throttler&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;Throttler&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This instance will block users the first time for one second. The second time for two, and so on.&lt;/p&gt;

&lt;p&gt;We use a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map" rel="noopener noreferrer"&gt;Map&lt;/a&gt; to store IP addresses and their corresponding data. A &lt;code&gt;Map&lt;/code&gt; is ideal because it handles frequent additions and deletions efficiently.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pro Tip: Use a Map for dynamic data that changes frequently. For static, unchanging data, an object is better. (Rabbit hole &lt;sup id="fnref1"&gt;1&lt;/sup&gt;)&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;When your endpoint receives a request, it extracts the user's IP address and consults the &lt;code&gt;Throttler&lt;/code&gt; to determine whether the request should be allowed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it Works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Case A: New or Inactive User&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If the IP is not found in the &lt;code&gt;Throttler&lt;/code&gt;, it’s either the user’s first request or they’ve been inactive long enough. In this case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allow the action.&lt;/li&gt;
&lt;li&gt;Track the user by storing their IP with an initial timeout.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Case B: Active User&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If the IP is found, it means the user has made previous requests. Here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check if the required wait time (based on the &lt;code&gt;timeoutSeconds&lt;/code&gt; array) has passed since their last block.&lt;/li&gt;
&lt;li&gt;If enough time has passed:&lt;/li&gt;
&lt;li&gt;Update the timestamp.&lt;/li&gt;
&lt;li&gt;Increment the timeout index (capped to the last index to prevent overflow).&lt;/li&gt;
&lt;li&gt;If not, deny the request.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this latter case, we need to check if enough time is passed since last block. We know which of the &lt;code&gt;timeoutSeconds&lt;/code&gt; we should refer thank to an &lt;code&gt;index&lt;/code&gt;. If not, simply bounce back. Otherwise update the timestamp.&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Throttler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;consume&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Case A&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;counter&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// At next request, will be found.&lt;/span&gt;
            &lt;span class="c1"&gt;// The index 0 of [1, 2, 4, 8, 16] returns 1.&lt;/span&gt;
            &lt;span class="c1"&gt;// That's the amount of seconds it will have to wait.&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&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="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// allowed&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Case B&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&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;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&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;allowed&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="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// denied&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Allow the call, but increment timeout for following requests.&lt;/span&gt;
        &lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutSeconds&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&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="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// allowed&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;When updating the index, it caps to the last index of &lt;code&gt;timeoutSeconds&lt;/code&gt;. Without it, &lt;code&gt;counter.index + 1&lt;/code&gt; would overflow it and next &lt;code&gt;this.timeoutSeconds[counter.index]&lt;/code&gt; access would result in a runtime error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Endpoint example
&lt;/h2&gt;

&lt;p&gt;This example shows how to use the &lt;code&gt;Throttler&lt;/code&gt; to limit how often a user can call your API. If the user makes too many requests, they’ll get an error instead of running the main logic.&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;throttler&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;Throttler&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&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;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&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;GET&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;getClientAddress&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;IP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getClientAddress&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;throttler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Too Many Requests&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="c1"&gt;// Read from DB, call OpenAI - do the thing.&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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;&lt;a href="/gifs/throttling-1.gif" class="article-body-image-wrapper"&gt;&lt;img src="/gifs/throttling-1.gif" alt="Throttling GIF example"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Note for Authentication
&lt;/h2&gt;

&lt;p&gt;When using rate limiting with login systems, you might face this issue:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A user logs in, triggering the &lt;code&gt;Throttler&lt;/code&gt; to associate a timeout with their IP.&lt;/li&gt;
&lt;li&gt;The user logs out or their session ends (e.g., logs out immediately, cookie expires with session and browsers crashes, etc.).&lt;/li&gt;
&lt;li&gt;When they try to log in again shortly after, the &lt;code&gt;Throttler&lt;/code&gt; may still block them, returning a &lt;code&gt;429 Too Many Requests&lt;/code&gt; error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To prevent this, use the user’s unique &lt;code&gt;userID&lt;/code&gt; instead of their IP for rate limiting. Also, you must reset the throttler state after a successful login to avoid unnecessary blocks.  &lt;/p&gt;

&lt;p&gt;Add a &lt;code&gt;reset&lt;/code&gt; method to the &lt;code&gt;Throttler&lt;/code&gt; class:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Throttler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;reset&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And use it after a successful login:&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;user&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&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;throttler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&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;validPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providedPassword&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;validPassword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;throttler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Clear throttling for the user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Managing Stale IP Records with Periodic Cleanup
&lt;/h2&gt;

&lt;p&gt;As your throttler tracks IPs and rate limits, it's important to think about how and when to remove IP records that are no longer needed. Without a cleanup mechanism, your throttler will continue to store records in memory, potentially leading to performance issues over time as the data grows.&lt;/p&gt;

&lt;p&gt;To prevent this, you can implement a cleanup function that periodically removes old records after a certain period of inactivity. Here's an example of how to add a simple cleanup method to remove stale entries from the throttler.&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Throttler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;// Capture the keys first to avoid issues during iteration (we use .delete)&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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;key&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;keys&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;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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="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;counter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Skip if the counter is already deleted (handles concurrency issues)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="c1"&gt;// If the IP is at the first timeout, remove it from storage&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;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&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="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&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="k"&gt;continue&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="c1"&gt;// Otherwise, reduce the timeout index and update the timestamp&lt;/span&gt;
            &lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&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="nx"&gt;counter&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A very simple way (but probably not the best) way to schedule the cleanup is with &lt;code&gt;setInterval&lt;/code&gt;:&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;throttler&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;Throttler&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&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;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&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;oneMinute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;
&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;throttler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;oneMinute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cleanup mechanism helps ensure that your throttler doesn't hold onto old records indefinitely, keeping your application efficient. While this approach is simple and easy to implement, it may need further refinement for more complex use cases (e.g., using more advanced scheduling or handling high concurrency).&lt;/p&gt;

&lt;p&gt;With periodic cleanup, you prevent memory bloat and ensure that users who haven’t attempted to make requests in a while are no longer tracked - this is a first step toward making your rate-limiting system both scalable and resource-efficient.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;If you’re feeling adventurous, you may be interested into reading &lt;a href="https://v8.dev/blog/fast-properties" rel="noopener noreferrer"&gt;how properties are allocared&lt;/a&gt; and &lt;a href="https://v8.dev/blog/hash-code" rel="noopener noreferrer"&gt;how it changes&lt;/a&gt;. Also, why not, about &lt;a href="https://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html" rel="noopener noreferrer"&gt;VMs optmizations like inline caches&lt;/a&gt;, which is particularly favored by &lt;a href="https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html" rel="noopener noreferrer"&gt;monomorphism&lt;/a&gt;. Enjoy. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
      <category>node</category>
    </item>
    <item>
      <title>Optimizing Three.js: 4 Key Techniques</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Fri, 29 Nov 2024 13:50:56 +0000</pubDate>
      <link>https://forem.com/didof/optimizing-threejs-4-key-techniques-4lad</link>
      <guid>https://forem.com/didof/optimizing-threejs-4-key-techniques-4lad</guid>
      <description>&lt;p&gt;Code can be art. Whether it's in clever syntax, elegant data structures, or refined interactions, there’s beauty only programmers see—and that’s fine.&lt;/p&gt;

&lt;p&gt;But code can also create something visually stunning, something everyone can appreciate. This is where tools like &lt;a href="https://threejs.org/" rel="noopener noreferrer"&gt;Three.js&lt;/a&gt; shine. However, Three.js can be heavy, especially when used in a dynamic web page accessed by devices with varying computational power.&lt;/p&gt;

&lt;p&gt;If you’re like me, adding multiple Three.js scenes to your site (as I do on &lt;a href="https://didof.dev" rel="noopener noreferrer"&gt;didof.dev&lt;/a&gt;), you’ll need optimizations. Here are three practical techniques to keep performance in check.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Originally posted on &lt;a href="https://www.didof.dev/blog/3d/optimize-threejs-4-key-techniques" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Load Scenes Only When Needed
&lt;/h2&gt;

&lt;p&gt;Don’t load a scene if it’s not visible. This applies to any heavy graphical component. The best tool for this is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API" rel="noopener noreferrer"&gt;&lt;code&gt;IntersectionObserver&lt;/code&gt;&lt;/a&gt;, which detects when an element enters the viewport. Here's how I handle it in &lt;a href="https://svelte.dev/docs/kit/introduction" rel="noopener noreferrer"&gt;SvelteKit&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&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;browser&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="s1"&gt;$app/environment&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onMount&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="s1"&gt;svelte&lt;/span&gt;&lt;span class="dl"&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;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLDivElement&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;download&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;browser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;onMount&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&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;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;entry&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;download&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="c1"&gt;// we need this once only&lt;/span&gt;
                    &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&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="c1"&gt;// ref has been bound by Svelte since we are in onMount&lt;/span&gt;
            &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;bind:this=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;#if&lt;/span&gt; &lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- let SvelteKit handle the code splitting --&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;#await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./three-scene.svelte&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            Loading
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;:then&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;module.default&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;:catch&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;/await&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;/if&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pause Scenes Out of View
&lt;/h2&gt;

&lt;p&gt;If a scene isn’t visible, stop rendering it. Most tutorials focus on a single fullscreen scene, but for sites with multiple scenes, pausing hidden ones saves resources.&lt;/p&gt;

&lt;p&gt;Here’s a snippet using &lt;code&gt;IntersectionObserver&lt;/code&gt; to control a scene’s animation loop:&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;function&lt;/span&gt; &lt;span class="nf"&gt;tick&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;elapsedTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElapsedTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Update your scene (e.g. set uniforms, move/rotate geometries...)&lt;/span&gt;

    &lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Start the rendering&lt;/span&gt;
&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAnimationLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once again, our friend &lt;code&gt;IntersectionObserver&lt;/code&gt; comes to our aid.&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;let&lt;/span&gt; &lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Clock&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;renderer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WebGLRenderer&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;browser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;onMount&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&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;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;entry&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAnimationLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// resume&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAnimationLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// pause&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Scene setup...&lt;/span&gt;

        &lt;span class="k"&gt;return &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="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Other cleanup...&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;h2&gt;
  
  
  Adjust Shader Workload for Viewport Size
&lt;/h2&gt;

&lt;p&gt;Devices with smaller screens are often less powerful. Adapt your shader’s computational workload accordingly. For example, reduce the number of octaves used in a fractal shader based on the viewport width:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;from the browser...&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ThreeScene&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;./three-scene.svelte&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;browser&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="s1"&gt;$app/environment&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;octaves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;680&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;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;ThreeScene&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;octaves&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;...through three.js...&lt;/em&gt;&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;material&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ShaderMaterial&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;vertexShader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;fragmentShader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;uniforms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;uOctaves&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Three&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;octaves&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// coming as $prop&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;&lt;em&gt;...finally, in the shader.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="k"&gt;uniform&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;uOctaves&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="kt"&gt;float&lt;/span&gt; &lt;span class="n"&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;uOctaves&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&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="n"&gt;elevation&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;simplexNoise2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warpedPosition&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;uPositionFrequency&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pow&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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;  &lt;span class="n"&gt;pow&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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&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="mi"&gt;0&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 approach dynamically balances performance and visual quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the Browser Handle Clean up
&lt;/h2&gt;

&lt;p&gt;Here’s where things get tricky. &lt;strong&gt;Three.js doesn’t automatically clean up memory&lt;/strong&gt;, and you need to manually track and dispose of objects like geometries, textures, and materials. If you skip this, memory usage grows every time you navigate away and back, eventually crashing the browser.&lt;/p&gt;

&lt;p&gt;Let me share what I observed on my homepage:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Initial memory usage&lt;/em&gt;: 22.4MB&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbmob8elko1ejquwb0nwd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbmob8elko1ejquwb0nwd.png" alt="homepage allocates 22 megabytes on the heap" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After soft-navigation to another page&lt;/em&gt;: 28.6MB (even though that page was static HTML).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvnj3xwkvmatvhxokhrf3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvnj3xwkvmatvhxokhrf3.png" alt="docs after homepage allocates 28 megabytes" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After repeated navigation back and forth&lt;/em&gt;: Memory usage kept climbing until the browser crashed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0eqb5q6rkrvv1pfi65e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0eqb5q6rkrvv1pfi65e.png" alt="soft-navigating-without cleanup will result in crash" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why? Because Three.js objects weren’t being disposed of properly. And despite extensive research, I couldn’t find a reliable way to fully clean up memory in modern frameworks.&lt;/p&gt;

&lt;p&gt;Here’s the simplest solution I found: &lt;strong&gt;force a hard-reload when leaving pages with Three.js scenes&lt;/strong&gt;. A hard-reload lets the browser:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new page context.&lt;/li&gt;
&lt;li&gt;Perform garbage collection on the old page (leaving cleanup to the browser).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In &lt;code&gt;SvelteKit&lt;/code&gt;, this is easy with &lt;a href="https://svelte.dev/docs/kit/link-options#data-sveltekit-reload" rel="noopener noreferrer"&gt;data-sveltekit-reload&lt;/a&gt;. Just enable it for pages with scenes:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;homepage's +server.page.ts&lt;/em&gt;&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;load&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;sveltekitReload&lt;/span&gt;&lt;span class="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For navigation links, pass this value dynamically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&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;page&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="s1"&gt;$app/stores&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/docs"&lt;/span&gt; &lt;span class="na"&gt;data-sveltekit-reload=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;$page&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="nx"&gt;sveltekitReload&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Docs&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If you use a generic &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; component, you only need to implement this once.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This approach isn’t perfect—it disables smooth client-side routing for specific pages—but &lt;strong&gt;it keeps memory in check and prevents crashes&lt;/strong&gt;. For me, that trade-off is worth it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;These optimizations have worked well for me, but the question remains: how do we properly clean up Three.js objects in modern frameworks? If you’ve found a reliable solution, I’d love to hear from you!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Radial Gradient Generator</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Thu, 28 Nov 2024 12:37:09 +0000</pubDate>
      <link>https://forem.com/didof/radial-gradient-generator-6gj</link>
      <guid>https://forem.com/didof/radial-gradient-generator-6gj</guid>
      <description>&lt;p&gt;After over five years in the IT industry, I've accumulated a wealth of internal tools and resources that I believe could benefit others. To share this knowledge, I've decided to launch a &lt;a href="https://didof.dev/free-tools" rel="noopener noreferrer"&gt;dedicated section on my personal website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Today, I'm excited to introduce the Radial Gradient Generator—a simple, web-based tool that allows you to create stunning radial gradients effortlessly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://didof.dev/free-tools/radial-gradient-generator" rel="noopener noreferrer"&gt;Go to Radial Gradient Generator&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it offers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stackable Shades
&lt;/h3&gt;

&lt;p&gt;Add as many shades as you like to create complex gradients.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customizable Options
&lt;/h3&gt;

&lt;p&gt;Define the color, position, and blur for each shade to achieve your desired look.&lt;/p&gt;

&lt;h3&gt;
  
  
  Easy Integration
&lt;/h3&gt;

&lt;p&gt;With just one click, you can copy your gradient and paste it directly into your website. There are no dependencies of course.&lt;/p&gt;

&lt;h2&gt;
  
  
  Examples
&lt;/h2&gt;

&lt;p&gt;These gradients serve as perfect placeholders or can be used to enhance your designs. I left &lt;a href="https://didof.dev/blog/free-tools/radial-gradient-generator#examples" rel="noopener noreferrer"&gt;some examples on my website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Feel free to use the Radial Gradient Generator and let me know your thoughts! Your feedback is invaluable as I continue to improve this tool and share more resources in the future.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>tooling</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to have Perfect Contrast of Text Color on Any Background in TailwindCSS</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Fri, 17 May 2024 10:11:40 +0000</pubDate>
      <link>https://forem.com/didof/how-to-have-perfect-contrast-of-text-color-on-any-background-in-tailwindcss-4cbh</link>
      <guid>https://forem.com/didof/how-to-have-perfect-contrast-of-text-color-on-any-background-in-tailwindcss-4cbh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;You can read the &lt;a href="https://easypagego.com/blog/perfect-contrast-text-color-on-any-background/" rel="noopener noreferrer"&gt;interactive version of this blog post&lt;/a&gt;!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;In recent years I have developed a good number of websites. The basic structure is usually similar among them, but there are some tasks that always require a lot of effort. And, coincidentally, they are precisely the &lt;strong&gt;most tedious&lt;/strong&gt; and complicated ones. Especially &lt;strong&gt;styling and choosing the colors of the website&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There are quite a few palette generators around the web, but all of them leave you with just a bunch of &lt;em&gt;hexs&lt;/em&gt; and &lt;em&gt;rgbs&lt;/em&gt; — it’s up to you to join those into your system.&lt;/p&gt;

&lt;p&gt;As if managing a theme was not enough, you were perhaps required to add the &lt;strong&gt;dark mode&lt;/strong&gt;! The &lt;strong&gt;TailwindCSS&lt;/strong&gt; team proposes to do this by wandering around the template, adding classes prefixed with &lt;code&gt;dark:&lt;/code&gt; to instruct each element about its appearance when the selected theme is dark.&lt;/p&gt;

&lt;p&gt;This is imho not optimal since, in addition to bloating the template even more, it would require manual work should I be asked to change some colors. And what if I want a third or even fourth theme?&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Problem
&lt;/h3&gt;

&lt;p&gt;I’ve been living with this problem peacefully until, after spending too many days hunting for html tags to slightly change their shades, I realized that, alas, &lt;em&gt;the text was no longer readable&lt;/em&gt; - &lt;strong&gt;it lacked contrast&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Lighthouse accessibility check, unconcerned about my emotional state, lowered the score.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I don’t want to have to think about this problem anymore.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;I have come up with a system that allows for an unlimited number of themes, each with an unlimited amount of colors. Most importantly, &lt;strong&gt;that ensures that the text is always readable on any color&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Basic usage involves configuring at &lt;strong&gt;least two themes&lt;/strong&gt;, &lt;em&gt;light mode&lt;/em&gt; and &lt;em&gt;dark mode&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Each theme requires at &lt;em&gt;least five colors&lt;/em&gt;. They are &lt;code&gt;background&lt;/code&gt;, &lt;code&gt;neutral&lt;/code&gt; (useful for card backgrounds), &lt;code&gt;primary&lt;/code&gt;, &lt;code&gt;secondary&lt;/code&gt; and &lt;code&gt;accent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No one is stopping you from adding others like &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;… you name it — it is completely configurable!&lt;/p&gt;

&lt;p&gt;For each of them, the TailwindCSS utilities and components are produced and available. It means you can not only add a &lt;code&gt;bg-secondary-300&lt;/code&gt; here and a &lt;code&gt;border-t-neutral-500&lt;/code&gt; there, but you get fancy stuff for free — IDE autocomplete included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gradient-to-br
  from-primary-300 via-accent-500 to-secondary-700
  shadow-accent-500"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqbpgntv8j2phxz3rzzro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqbpgntv8j2phxz3rzzro.png" alt="A square with a fancy colored background and shadow" width="396" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Text perfect contrast
&lt;/h3&gt;

&lt;p&gt;Thanks to a simple plugin (no need to install anything), some contrast dedicated classes are generated (and available in the IDE autocomplete). They follow the pattern &lt;code&gt;text-wacg-&amp;lt;color&amp;gt;-&amp;lt;shade&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can try real-time widgets on my &lt;a href="https://easypagego.com/blog/perfect-contrast-text-color-on-any-background/#text-perfect-contrast" rel="noopener noreferrer"&gt;blog post&lt;/a&gt;!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvyl9ccyl0f2vxgprup99.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvyl9ccyl0f2vxgprup99.png" alt="The widget the helps appreciate the power of this strategy" width="800" height="208"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-primary-500 text-wacg-primary-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    This text will always be readable
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is easily obtainable with my &lt;a href="https://easypagego.com/tools/tailwindcss-accessible-colors/" rel="noopener noreferrer"&gt;free online tool&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Benefits
&lt;/h3&gt;

&lt;p&gt;Read more about the other convenient benefits:&lt;/p&gt;

&lt;h4&gt;
  
  
  Framework agnostic
&lt;/h4&gt;

&lt;p&gt;It is &lt;strong&gt;adoptable in any framework&lt;/strong&gt;: React, Next.js, Vue, Nuxt, Angular, Svelte, SvelteKit, Sapper, Laravel, Spring, Rails, Django, Express, Hugo, Solid.js, Astro.js, Preact, Ember.js, Alpine.js, LitElement, JQuery and any other I may have omitted.&lt;/p&gt;

&lt;h4&gt;
  
  
  Zero dependencies
&lt;/h4&gt;

&lt;p&gt;I have opted for a &lt;strong&gt;dependency-free solution&lt;/strong&gt;. As a developer, I do not like to install packages unless it is strictly necessary. Mainly because I know how dangerous it can be — supply chain attack gettin’ real!&lt;/p&gt;

&lt;p&gt;This solution is &lt;strong&gt;not a black box&lt;/strong&gt;, and a few paragraph below you can appreciate how simple yet powerful it is.&lt;/p&gt;

&lt;h4&gt;
  
  
  CSS Only
&lt;/h4&gt;

&lt;p&gt;This solution minimizes JavaScript usage at runtime, &lt;strong&gt;leveraging CSS for the majority of heavy lifting&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  IDE autocomplete
&lt;/h4&gt;

&lt;p&gt;All the palette colors are utilized to generate all the &lt;strong&gt;TailwindCSS utilities&lt;/strong&gt; (&lt;code&gt;bg-primary-500&lt;/code&gt;, &lt;code&gt;border-l-neutral-300&lt;/code&gt;, etc…), complete with &lt;em&gt;IDE autocompletion&lt;/em&gt;. Additionally, any unused elements are purged by TailwindCSS, ensuring they are not present in the final CSS output.&lt;/p&gt;

&lt;h4&gt;
  
  
  Browser support
&lt;/h4&gt;

&lt;p&gt;The system is based on two &lt;em&gt;CSS features&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;var&lt;/code&gt;, which allows the declaration and usage of cascading variables in stylesheets, has &lt;a href="https://caniuse.com/?search=CSS%20Variable%20(Custom%20Properties)" rel="noopener noreferrer"&gt;support on 97.32% of browsers&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;calc&lt;/code&gt;, which allows to calculate values directly into CSS, is &lt;a href="https://caniuse.com/?search=calc%20as%20CSS%20unit%20value" rel="noopener noreferrer"&gt;supported in 98.19% of all browsers&lt;/a&gt;.
Stay tuned to learn how it works — otherwise you can already use the &lt;a href="https://easypagego.com/tools/tailwindcss-accessible-colors/" rel="noopener noreferrer"&gt;free online tool&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  FaQ
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Q: Why do I have to explicitly set the &lt;code&gt;text-wacg&lt;/code&gt; class? Couldn’t it be implicit in the background one?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A: It could, but I learned to appreciate explicitness.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>tailwindcss</category>
      <category>webdev</category>
      <category>tooling</category>
      <category>themes</category>
    </item>
    <item>
      <title>Web Perf - Large Files</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Tue, 28 Nov 2023 16:30:00 +0000</pubDate>
      <link>https://forem.com/didof/web-perf-large-files-5a9j</link>
      <guid>https://forem.com/didof/web-perf-large-files-5a9j</guid>
      <description>&lt;p&gt;When you have a lot of information to show on the screen, there are two common choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pagination&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;streaming&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But you know what? You can also just send the whole thing to the user. Let's explore this straightforward approach together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Headers
&lt;/h2&gt;

&lt;p&gt;We'll explore three crucial headers - &lt;code&gt;Content-Length&lt;/code&gt;, &lt;code&gt;Content-Encoding&lt;/code&gt;, and &lt;code&gt;Transfer-Encoding&lt;/code&gt; - and see how different combinations of these headers can influence the rendering on the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length" rel="noopener noreferrer"&gt;&lt;code&gt;Content-Length&lt;/code&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;It specifies the size of the payload in bytes. It helps the recipient (in our case, the browser) know how much data to expect. It's like a heads-up, ensuring everyone is on the same page about the amount of content coming their way.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding" rel="noopener noreferrer"&gt;&lt;code&gt;Content-Encoding&lt;/code&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;It tells the browser how the content is encoded or compressed. It's like a secret code that both the server and the browser understand. Common encodings include gzip and deflate. When the browser sees this header, it knows how to decode the content for a smooth display.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding" rel="noopener noreferrer"&gt;&lt;code&gt;Transfer-Encoding&lt;/code&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This less-known header &lt;strong&gt;handles the encoding of the message itself during transmission&lt;/strong&gt;. It can be sent all at once (like a single big parcel) or in smaller pieces (like breaking it into multiple smaller packages).&lt;/p&gt;




&lt;h2&gt;
  
  
  Server
&lt;/h2&gt;

&lt;p&gt;To see how the browser reacts to different combinations of these headers, let's set up a basic Node.js HTTP server.&lt;/p&gt;

&lt;p&gt;The server is designed to handle incoming requests for any path, offering flexibility through optional query parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;content-length&lt;/code&gt;: If set to &lt;code&gt;true&lt;/code&gt;, this parameter adds the &lt;code&gt;Content-Length&lt;/code&gt; header to the response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;transfer&lt;/code&gt;: This parameter sets the &lt;code&gt;Transfer-Encoding&lt;/code&gt; header, with options for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;identity&lt;/code&gt;: Instruct the server to send the document as a whole.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chunked&lt;/code&gt;: Directs the server to send the document in chunks.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;gzip&lt;/code&gt;: If set to &lt;code&gt;true&lt;/code&gt;, the server retrieves the zipped version of the file. In this case, the &lt;code&gt;Content-Length&lt;/code&gt; (if set) reflects the size of the zipped version.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/didof/post-web-perf/tree/main/compare_headers" rel="noopener noreferrer"&gt;Support Code&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
  &lt;code&gt;index.mjs&lt;/code&gt;
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServer&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;http&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;combinations&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;./src/utils.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;}&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://127.0.0.1:8000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Ready query params&lt;/span&gt;
  &lt;span class="kd"&gt;const&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;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-length&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;false&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;gzip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gzip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;false&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;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;identity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Either index.html or index.html.gz&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;index.html&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="nx"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.gz&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;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stats&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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;text/html&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="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Length&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&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;gzip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Encoding&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;gzip&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="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Transfer-Encoding&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;identity&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;combinations&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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;

&lt;p&gt;
  &lt;code&gt;src/utils.mjs&lt;/code&gt;
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolve&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;path&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stat&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;fs/promises&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;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="dl"&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;getView&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;encoding&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;filepath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;views&lt;/span&gt;&lt;span class="dl"&gt;"&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="kd"&gt;const&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="nx"&gt;stats&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filepath&lt;/span&gt;&lt;span class="p"&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;could not retrieve 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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;could not retrieve stats&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="nx"&gt;data&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="nx"&gt;stats&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Ignore if not running on your machine.&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;createURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Use pathname so that in Developer Tools you can filter out the favicon.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;big&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;http://127.0.0.1:8000&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="nx"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gzip&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;true&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="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-length&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;true&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="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;identity&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;true&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Ignore if not running on your machine.&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;combinations&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;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;createURL&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;gzip&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&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="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;length&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&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="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;identity&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&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="nx"&gt;urls&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="nf"&gt;createURL&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="nx"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;identity&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="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="nx"&gt;urls&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;

&lt;p&gt;The content within the HTML files is not the focus; their substantial size is (AKA they're LARGE).&lt;/p&gt;

&lt;p&gt;These files are maintained in two versions: plain and gzipped. While it's generally not a recommended practice for real servers to adopt dual storage, here the intent is to avoid introducing additional overhead in the case of zipping.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;I'll now test the server using Firefox, monitoring how response times vary with changes in the headers. If you're following along on your machine, make sure to disable the cache and consider checking the 'Preserve logs' checkbox for a more accurate observation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4evzk52b6mxczkuji1sq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4evzk52b6mxczkuji1sq.png" alt="Screenshot of Network tab showing what is descripted below." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Initially, we make a request without query parameters, letting the Node.js HTTP server handle it by default. The default &lt;code&gt;Transfer-Encoding&lt;/code&gt; observed is &lt;code&gt;chunked&lt;/code&gt;, aligning with the behavior of passing &lt;code&gt;?transfer=chunked&lt;/code&gt;. Node.js aims to be non-blocking, and this choice ensures smoother processing.&lt;/p&gt;

&lt;p&gt;Now, let's spice things up by passing the query parameter &lt;code&gt;?transfer=identity&lt;/code&gt;. This time, the request takes notably longer to complete.&lt;/p&gt;

&lt;p&gt;To remedy this, we introduce the &lt;code&gt;Content-Length&lt;/code&gt; header with &lt;code&gt;?content-length=true&amp;amp;identity=true&lt;/code&gt;, resulting in a significant reduction in duration. It's like mailing a package in one piece. Including the &lt;code&gt;Content-Length&lt;/code&gt; header is the friendly note that says, 'Hey, your package is this big!'. Without it, the client might fumble guessing the size, leading to some awkward data processing moments. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔑 message &lt;br&gt;
In 'identity' mode, be a good server and always attach that &lt;code&gt;Content-Length&lt;/code&gt; header.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As a final observation, we note that the presence of the &lt;code&gt;Content-Length&lt;/code&gt; has no impact when the transfer method is set to chunked.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔑 message&lt;br&gt;
Not only there's no need for the &lt;code&gt;Content-Length&lt;/code&gt; header`, but &lt;strong&gt;using both is actually contradictory&lt;/strong&gt;.&lt;br&gt;
In 'chunked' encoding, the size of each chunk is self-contained, and a final zero-size chunk does the job of marking the end of the response.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Compression
&lt;/h3&gt;

&lt;p&gt;When using the gzip-compressed resource, the behavior aligns with what we've just explored. In the &lt;code&gt;identity&lt;/code&gt; transfer mode, &lt;strong&gt;it remains crucial to provide information about the content length&lt;/strong&gt;, regardless of the content encoding.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkwri5znoh3gjuhoga08.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkwri5znoh3gjuhoga08.png" alt="Screenshot of Network tab showing what is descripted below." width="641" height="118"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let's talk about compression benefits and a trade-off. Opting for gzip compression offers two wins:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;it conserves disk storage on your server.&lt;/li&gt;
&lt;li&gt;it trims down on bandwidth usage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, there's a catch - &lt;strong&gt;the browser has to roll up its sleeves and put in a bit more effort to decompress&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;If you are interested in &lt;strong&gt;Web Performance&lt;/strong&gt; you definitely need to know about &lt;a href="https://dev.to/didof/the-art-of-efficient-web-browsing-public-resources-27hl"&gt;&lt;strong&gt;Web Caching&lt;/strong&gt; (posts series)&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>performance</category>
    </item>
    <item>
      <title>Web Caching - Cache-Control max-age, stale-while-revalidate</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Thu, 23 Nov 2023 17:00:00 +0000</pubDate>
      <link>https://forem.com/didof/web-caching-cache-control-3plb</link>
      <guid>https://forem.com/didof/web-caching-cache-control-3plb</guid>
      <description>&lt;p&gt;Until now, thanks to &lt;code&gt;Last-Modified/If-Modified-Since&lt;/code&gt; or &lt;code&gt;ETag/If-None-Match&lt;/code&gt; we mainly saved on bandwidth. However, &lt;strong&gt;the server always had to process each request&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The server can instruct the client about &lt;strong&gt;using the stored resources&lt;/strong&gt; for a certain duration, deciding &lt;strong&gt;if and when the client should revalidate&lt;/strong&gt; the content and whether or not to &lt;strong&gt;do so in the background&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/didof/post-caching" rel="noopener noreferrer"&gt;Support code&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Real-world endpoints are not as instantaneous as those in this tutorial. The response &lt;strong&gt;may take milliseconds to generate&lt;/strong&gt;, without even considering the location of the server relative to the requester!&lt;/p&gt;

&lt;p&gt;Let's exacerbate the asynchronicity between server and client to highlight the need for the &lt;code&gt;Cache-Control&lt;/code&gt; Header.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/utils.mjs&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;duration&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;duration&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;Based on the &lt;a href="https://dev.to/didof/web-caching-etagif-none-match-517n"&gt;previously implemented &lt;code&gt;/only-etag&lt;/code&gt; endpoint&lt;/a&gt;, register &lt;code&gt;/cache-control-with-etag&lt;/code&gt;. For the time being, it's identical, except that &lt;em&gt;it waits three seconds&lt;/em&gt; before responding.&lt;br&gt;
Also, add some log before dispatching the response&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Load on the server!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// computation;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// simulate async.&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;etag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createETag&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ETag&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;etag&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;ifNoneMatch&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;Headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;If-None-Match&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="nx"&gt;ifNoneMatch&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;304&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's visualize the problem. When you request a page from the browser, &lt;strong&gt;it enters a loading state&lt;/strong&gt; for three seconds. Even if you refresh within that time, the browser &lt;strong&gt;makes the request anew&lt;/strong&gt;, regardless of the &lt;code&gt;ETag&lt;/code&gt; or the &lt;code&gt;Last-Modified&lt;/code&gt; mechanisms. The page content persists because you're essentially staying on the same page. To observe the behavior more clearly, &lt;em&gt;try reopening the page from a new tab or starting from a different site&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Most importantly, &lt;strong&gt;the server is hit on every request&lt;/strong&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  max-age
&lt;/h2&gt;

&lt;p&gt;It is possible to instruct the browser to use the cached version for a certain duration. The server will set the Response Header &lt;code&gt;Cache-Control&lt;/code&gt; with a value of &lt;code&gt;max-age=&amp;lt;seconds&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Load on the server!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// db retrieval and templating;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// simulate async.&lt;/span&gt;

  &lt;span class="c1"&gt;// Instruct the browser to use the cached resource&lt;/span&gt;
  &lt;span class="c1"&gt;// for 60 * 60 seconds = 1 hour&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&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;max-age: 3600&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// as seen before&lt;/span&gt;
  &lt;span class="c1"&gt;// 200 or 304&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give it another try now, and request the page. The first request makes the browser load for three seconds. If you open the same page in another tab, you'll notice &lt;strong&gt;it's already there&lt;/strong&gt;, and &lt;strong&gt;the server wasn't contacted&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This behavior persists for the specified seconds. If, after the cache expires, a new request returns a &lt;code&gt;304 Not Modified&lt;/code&gt; (thanks to the &lt;code&gt;If-None-Match&lt;/code&gt; header), the resource will be newly cached for that amount of time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;➕ Better UX&lt;/li&gt;
&lt;li&gt;➕ Less load on the server&lt;/li&gt;
&lt;li&gt;❔ If the resource changes, the client remains unaware until the cache expires. After expiration, with a different &lt;strong&gt;Etag&lt;/strong&gt;, a new version will be displayed and cached.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;When attempting a refresh while staying on the page, you might observe the loading delay, indicating that the server is being contacted. Make sure you're not doing a &lt;strong&gt;hard refresh&lt;/strong&gt;, as it overrides the described behavior.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  stale-while-revalidate
&lt;/h3&gt;

&lt;p&gt;If your application needs resource validation but you still want to show the cached version for a better user experience when available, you can use the &lt;code&gt;stale-while-revalidate=&amp;lt;seconds&amp;gt;&lt;/code&gt; directive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&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;max-age=120, stale-while-revalidate=300&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, the browser is instructed to cache the response for 2 minutes. &lt;strong&gt;Once this period elapses&lt;/strong&gt;, if the resource is requested &lt;strong&gt;within&lt;/strong&gt; the next 5 minutes, the browser will use the cached resource (even if it's stale) but will perform a background validation call.&lt;/p&gt;

&lt;p&gt;I want to emphasize the "even if stale" by playing around with the endpoint configured as above. Only &lt;strong&gt;soft refreshes&lt;/strong&gt; are performed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;strong&gt;Info&lt;/strong&gt;&lt;br&gt;
Tap/click the next items to show the related graph.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
  &lt;b&gt;1.&lt;/b&gt; On the initial page request, it takes 3 seconds to load, and the response is cached for 2 minutes with an associated &lt;b&gt;ETag&lt;/b&gt;. The client will include this &lt;b&gt;ETag&lt;/b&gt; in the &lt;code&gt;If-None-Match&lt;/code&gt; header for subsequent requests.
  &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj06fitwxm1mlrok2qv4f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj06fitwxm1mlrok2qv4f.png" alt="graph of point 1." width="800" height="1345"&gt;&lt;/a&gt;&lt;br&gt;


&lt;/p&gt;

&lt;p&gt;
  &lt;b&gt;2.&lt;/b&gt; Close and reopen the browser (or request from another page) &lt;b&gt;within the 2-minute window&lt;/b&gt;: the page is &lt;b&gt;instantly shown without hitting the server&lt;b&gt;&lt;/b&gt;.&lt;/b&gt;
  &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F35m4y8lc82b5vkz2i0so.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F35m4y8lc82b5vkz2i0so.png" alt="graph of point 2." width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;


&lt;/p&gt;

&lt;p&gt;
  &lt;b&gt;3.&lt;/b&gt; After the 2-minute cache expires, &lt;b&gt;the server is contacted&lt;/b&gt;. The resource has not changed, the client receives &lt;code&gt;304 Not Modified&lt;/code&gt;. The cache expiry date is extended with the provided &lt;code&gt;max-age&lt;/code&gt; value. No need to update what the user is seeing.
  &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7m00fh11tcp34jseh5ep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7m00fh11tcp34jseh5ep.png" alt="graph of point 3." width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;


&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.&lt;/strong&gt; Now that the 2-minute window is open again, let's update the content of the resource using the db endpoint implemented in the &lt;a href="https://dev.to/didof/web-caching-etagif-none-match-517n"&gt;previous blog post&lt;/a&gt;. Basically, &lt;strong&gt;next time the ETag will not match&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8000/db &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "title": "ETag", "tag": "code" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
  &lt;b&gt;5.&lt;/b&gt; Still &lt;b&gt;within the 2-minute window&lt;/b&gt;, request the page again. Thanks to &lt;code&gt;max-age&lt;/code&gt;, the browser shows it immediately. It proceeds with background validation as already seen in step 3. But this time the &lt;b&gt;ETag&lt;/b&gt; does not match; the server responds with a &lt;code&gt;200 OK&lt;/code&gt; and provides a new &lt;b&gt;ETag&lt;/b&gt; (which overrides the previous entry in the cache).
  &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb0hooul1taitkerwihnz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb0hooul1taitkerwihnz.png" alt="graph of point 5." width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;


&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔑 message&lt;br&gt;
Although the displayed content is stale, the browser &lt;strong&gt;updates its cache silently&lt;/strong&gt;, &lt;strong&gt;preserving the user experience&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Request the page anew; this time, it finally displays the latest stored version. If within the newly restarted 2-minute window, the server won't be contacted.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;🔑 message&lt;br&gt;
Any request outside the time window indicated by stale-while-revalidate (which I recall, starts from the expiration of that of max-age), will behave like step 1 - blank state.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>cache</category>
      <category>performance</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Web Caching - ETag/If-None-Match</title>
      <dc:creator>Francesco Di Donato</dc:creator>
      <pubDate>Tue, 21 Nov 2023 15:40:00 +0000</pubDate>
      <link>https://forem.com/didof/web-caching-etagif-none-match-517n</link>
      <guid>https://forem.com/didof/web-caching-etagif-none-match-517n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/didof/post-caching" rel="noopener noreferrer"&gt;Support code&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the previous &lt;a href="https://dev.to/didof/the-art-of-efficient-web-browsing-public-resources-27hl"&gt;post&lt;/a&gt;, we explored the usefulness of the &lt;code&gt;Last-Modified&lt;/code&gt; Response Header and &lt;code&gt;If-Modified-Since&lt;/code&gt; Request Header. They work really well when dealing with an endpoint returning a file.&lt;/p&gt;

&lt;p&gt;What about &lt;strong&gt;data retrieved from a database&lt;/strong&gt; or &lt;strong&gt;assembled from different sources&lt;/strong&gt;?&lt;/p&gt;

&lt;center&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th&gt;Request&lt;/th&gt;
            &lt;th&gt;Response&lt;/th&gt;
            &lt;th&gt;Value example&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td&gt;&lt;code&gt;Last-Modified&lt;/code&gt;&lt;/td&gt;
            &lt;td&gt;&lt;code&gt;If-Modified-Since&lt;/code&gt;&lt;/td&gt;
            &lt;td&gt;&lt;code&gt;Thu, 15 Nov 2023 19:18:46 GMT&lt;/code&gt;&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;&lt;code&gt;ETag&lt;/code&gt;&lt;/td&gt;
            &lt;td&gt;&lt;code&gt;If-None-Match&lt;/code&gt;&lt;/td&gt;
            &lt;td&gt;&lt;code&gt;75e7b6f64078bb53b7aaab5c457de56f&lt;/code&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;/center&gt;

&lt;p&gt;Also here, we have a &lt;em&gt;tuple of headers&lt;/em&gt;. One must be provided by the requester (&lt;code&gt;ETag&lt;/code&gt;), while the other is returned by the sender (&lt;code&gt;If-None-Match&lt;/code&gt;). The value is a &lt;strong&gt;hash generated on the content of the response&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you want to go directly to using headers, go to the endpoint. Otherwise, observe (but don't spend too much time on) the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparation
&lt;/h2&gt;

&lt;p&gt;For simplicity, we use an in-memory DB. It is exposed via the endpoint &lt;code&gt;/db&lt;/code&gt;. It contains a list of &lt;code&gt;posts&lt;/code&gt;. Each post contains a &lt;code&gt;title&lt;/code&gt; and a &lt;code&gt;tag&lt;/code&gt;. Posts can be added via &lt;code&gt;POST&lt;/code&gt;, and modified via &lt;code&gt;PATCH&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Retrieval is via a &lt;code&gt;GET&lt;/code&gt; function, which optionally filters by &lt;code&gt;tag&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;
  &lt;code&gt;src/db.mjs&lt;/code&gt;
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getJSONBody&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;./utils.mjs&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;POSTS&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Caching&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;code&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Headers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;code&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dogs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animals&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&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;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;POSTS&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;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;tag&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;posts&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;default&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;db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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;getJSONBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something went wrong&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="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;POSTS&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;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PATCH&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;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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;getJSONBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something went wrong&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="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;POSTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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="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;

&lt;p&gt;
  &lt;code&gt;src/utils.mjs&lt;/code&gt;
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&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="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;getJSONBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;req&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;data&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="nx"&gt;chunk&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="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;req&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;error&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="nx"&gt;req&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;end&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&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;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;null&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;

&lt;h2&gt;
  
  
  Endpoint
&lt;/h2&gt;

&lt;p&gt;By registering the &lt;em&gt;db&lt;/em&gt;, we will be able to modify the content of the responses in real-time, appreciating the usefulness of ETag.&lt;br&gt;
Also, let's register and create the &lt;code&gt;/only-etag&lt;/code&gt; endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/index.mjs&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;createServer&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;http&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;db&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;.src/db.mjs&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;onlyETag&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;./src/only-etag.mjs&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getURL&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;./src/utils.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/only-etag&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;onlyETag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/db&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&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="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Exposed on http://127.0.0.1:8000&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;onlyETag&lt;/code&gt; endpoint accepts an optional query parameter &lt;code&gt;tag&lt;/code&gt;. If present, it is used &lt;em&gt;to filter the retrieved posts&lt;/em&gt;.&lt;br&gt;
Thus, the template is loaded in memory.&lt;/p&gt;

&lt;p&gt;
  &lt;code&gt;src/views/posts.html&lt;/code&gt;
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Tag: %TAG%&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;%POSTS%&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"GET"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"tag"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"tag"&lt;/span&gt; &lt;span class="na"&gt;autofocus&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"filter"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When submitted, the form uses as &lt;code&gt;action&lt;/code&gt; the current route (&lt;code&gt;/only-etag&lt;/code&gt;) appending as query parameter the &lt;code&gt;name&lt;/code&gt; attribute. For example, typing &lt;code&gt;code&lt;/code&gt; in the input and submitting the form would result in &lt;code&gt;GET /only-etag?name=code&lt;/code&gt;), &lt;strong&gt;No JavaScript required&lt;/strong&gt;!&lt;/p&gt;





&lt;/p&gt;

&lt;p&gt;And the posts are injected into it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;db&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;./db.mjs&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createETag&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;./utils.mjs&lt;/span&gt;&lt;span class="dl"&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;default&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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;text/html&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;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tag&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;posts&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&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;errView&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;getView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;posts&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="nx"&gt;errView&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Internal Server Error&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="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;%TAG%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;%POSTS%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/li&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ETag&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;createETag&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you notice, before dispatching the response, the &lt;strong&gt;ETag&lt;/strong&gt; is generated and included under the &lt;code&gt;ETag&lt;/code&gt; Response header.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/utils.mjs&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;createHash&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;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&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;createETag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resource&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="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;md5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&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;blockquote&gt;
&lt;p&gt;Changing the content of the resource changes the &lt;b&gt;E&lt;/b&gt;ntity &lt;strong&gt;Tag&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Performing the request from the browser you can inspect the Response Headers via the Network tab of the Developer Tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text/html&lt;/span&gt;
&lt;span class="na"&gt;ETag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4775245bd90ebbda2a81ccdd84da72b3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you refresh the page, you'll notice the browser adding the &lt;code&gt;If-None-Match&lt;/code&gt; header to the request. The value corresponds of course to the one it received before.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/only-etag&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;If-None-Match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4775245bd90ebbda2a81ccdd84da72b3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As seen in the previous posts per &lt;code&gt;Last-Modified&lt;/code&gt; and &lt;code&gt;If-Modified-Since&lt;/code&gt;, let's instruct the endpoint to deal with &lt;code&gt;If-None-Match&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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;text/html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;retrieve &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// as seen before&lt;/span&gt;
  &lt;span class="nx"&gt;load&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// as seen before&lt;/span&gt;
  &lt;span class="nx"&gt;fill&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// as seen before&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;etag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createETag&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ETag&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;etag&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;ifNoneMatch&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;Headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;If-None-Match&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="nx"&gt;ifNoneMatch&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;304&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Indeed, subsequent requests on the same resource return &lt;code&gt;304 Not Modified&lt;/code&gt;, instructing the browser to use previously stored resources. Let's request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/only-etag&lt;/code&gt; three times in a row;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/only-etag?tag=code&lt;/code&gt; twice;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/only-etag?tag=animals&lt;/code&gt; twice;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/only-etag&lt;/code&gt;, without tag, once again;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbspcf1w7gguslu07v6qa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbspcf1w7gguslu07v6qa.png" alt="A screenshot of the Developer Tools showing what is listed above." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The presence of the query parameter determines a change in response, thus in &lt;strong&gt;ETag&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Notice the last one. It does not matter that there have been other requests in the meantime; the browser keeps a map of requests (including the query parameters) and ETags.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Detect entity change
&lt;/h3&gt;

&lt;p&gt;To further underscore the significance of this feature, let's add a new post to the DB from another process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8000/db &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "title": "ETag", "tag": "code" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And request again &lt;code&gt;/only-etag?tag=code&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpo0hmpoord616pokabe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpo0hmpoord616pokabe.png" alt="The same cached response is not useful anymore since the returned ETag is now different." width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
After the &lt;em&gt;db&lt;/em&gt; has been updated, the same request generated a different &lt;strong&gt;ETag&lt;/strong&gt;. Thus, the server sent the client a new version of the resource, with a newly generated &lt;strong&gt;ETag&lt;/strong&gt;. Subsequent requests will fall back to the expected behavior.&lt;/p&gt;

&lt;p&gt;The same happens if we modify an element of the response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; PATCH http://127.0.0.1:8000/db &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "title": "Amazing Caching", "index": 0 }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F960g0w3ycv1boii1976b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F960g0w3ycv1boii1976b.png" alt="The same behaviour seen before is replicated if the content changes once again." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;While ETag is a more versatile solution, applicable regardless of the data type since it is content-based, it should be considered that &lt;strong&gt;the server must still retrieve and assemble the response&lt;/strong&gt;, then pass it into the hashing function and compare it with the received value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thanks to another header, &lt;code&gt;Cache-Control&lt;/code&gt;, it is possible to optimize the number of requests the server has to process.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cache</category>
      <category>performance</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
