惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
Know Your Adversary
Know Your Adversary
Google DeepMind News
Google DeepMind News
A
Arctic Wolf
P
Privacy & Cybersecurity Law Blog
云风的 BLOG
云风的 BLOG
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
Project Zero
Project Zero
L
LangChain Blog
N
News and Events Feed by Topic
博客园 - Franky
Last Week in AI
Last Week in AI
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Scott Helme
Scott Helme
T
The Exploit Database - CXSecurity.com
P
Proofpoint News Feed
Blog — PlanetScale
Blog — PlanetScale
www.infosecurity-magazine.com
www.infosecurity-magazine.com
W
WeLiveSecurity
月光博客
月光博客
博客园_首页
美团技术团队
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
腾讯CDC
Latest news
Latest news
WordPress大学
WordPress大学
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Spread Privacy
Spread Privacy
Attack and Defense Labs
Attack and Defense Labs
量子位
L
LINUX DO - 热门话题
C
CERT Recently Published Vulnerability Notes
Webroot Blog
Webroot Blog
L
Lohrmann on Cybersecurity
aimingoo的专栏
aimingoo的专栏
T
Troy Hunt's Blog
Security Latest
Security Latest
小众软件
小众软件
Cloudbric
Cloudbric
Hacker News: Ask HN
Hacker News: Ask HN
S
Secure Thoughts
雷峰网
雷峰网
T
Threat Research - Cisco Blogs
H
Hacker News: Front Page
IT之家
IT之家
Simon Willison's Weblog
Simon Willison's Weblog

Deno

Deno 2.8 | Deno Claw Patrol: an open-source security firewall for agents | Deno Fresh 2.3: Zero JS by default, View Transitions, and Temporal support | Deno Deno 2.7: Temporal API, Windows ARM, and npm overrides | Deno Build a dinosaur runner game with Deno, pt. 6 | Deno Build a dinosaur runner game with Deno, pt. 5 | Deno Deno Deploy is Generally Available | Deno Introducing Deno Sandbox | Deno Build a dinosaur runner game with Deno, pt. 4 | Deno Build a dinosaur runner game with Deno, pt. 3 | Deno Build a dinosaur runner game with Deno, pt. 2 | Deno React / Next.js Denial-of-Service Vulnerability: Deno Deploy users protected | Deno Deno 2.6: dx is the new npx | Deno Build a dinosaur runner game with Deno, pt. 1 | Deno React Server Functions / Next.js Vulnerability: Deno Deploy users protected | Deno My highlights from the new Deno Deploy | Deno Deno's Other Open Source Projects | Deno How Deno protects against npm exploits | Deno Help Us Raise $200k to Free JavaScript from Oracle | Deno Deno 2.5: Permissions in the config file | Deno Fresh 2.0 Graduates to Beta, Adds Vite Support | Deno Deno 2.4: deno bundle is back | Deno JavaScript™ Trademark Update | Deno What's coming to JavaScript | Deno A brief history of JavaScript | Deno Reports of Deno's Demise Have Been Greatly Exaggerated | Deno An Update on Fresh | Deno How Plaid migrated 100 services to a new database platform 5x faster with Deno | Deno Deno 2.3: Improved deno compile, local npm packages, and more | Deno Add JSR packages with pnpm and Yarn | Deno Zero-config Debugging with Deno and OpenTelemetry | Deno Exploring Art with TypeScript, Jupyter, Polars, and Observable Plot | Deno Deno v Oracle Update 3: Fighting the JavaScript Trademark | Deno Build a custom RAG AI agent in TypeScript and Jupyter | Deno How to get deep traces in your Node.js backend with OTel and Deno | Deno toranoana.deno #20 登録受付中(2025年3月14日) | Deno Node just added TypeScript support. What does that mean for Deno? | Deno The Dino 🦕, the Llama 🦙, and the Whale 🐋 | Deno Publish a lint rule, get a prize | Deno Deno 2.2: OpenTelemetry, Lint Plugins, node:sqlite | Deno If you're not using npm specifiers, you're doing it wrong | Deno How Deno's documentation is evolving | Deno Oracle justified its JavaScript trademark with Node.js—now it wants that ignored | Deno Introducing the JSR open governance board | Deno Intro to Wasm in Deno | Deno Announcing OpenAI on JSR | Deno Deno in 2024 | Deno Goodbye WinterCG, welcome WinterTC | Deno Build a SolidJS app with Deno | Deno Run your Next.js SSR app on Deno Deploy | Deno Solve Advent of Code 2024 with Deno and Win Prizes! | Deno Deno v. Oracle: Canceling the JavaScript Trademark | Deno Deno 2.1: Wasm Imports and other enhancements | Deno Build a Typesafe API with tRPC and Deno | Deno Self-contained Executable Programs with Deno Compile | Deno Build a Database App with Drizzle ORM and Deno | Deno Introducing your new JavaScript package manager: Deno | Deno Announcing Growthbook on JSR | Deno Build an Astro site with Deno | Deno How to convert CommonJS to ESM | Deno Announcing Deno 2 | Deno The Final Touches: What’s New In v2.0.0-rc.10 | Deno Announcing Stable V8 Bindings for Rust | Deno Deno 2.0 Release Candidate | Deno Secure, efficient private npm registries with Cloudsmith and Deno | Deno Painting the Plane as We Fly It: Designing JSR | Deno Introducing Web Cache API support on Deno Deploy | Deno Deno 1.46: The Last 1.x Release | Deno Protect your cloud spend with new Deno Deploy spend limits | Deno What we got wrong about HTTP imports | Deno Benchmarking AWS Lambda Cold Starts Across JavaScript Runtimes | Deno Announcing Supabase on JSR | Deno Deno 1.45: Workspace and Monorepo Support | Deno Introducing KV Backup for Deno Subhosting | Deno A Gentle Intro to TypeScript | Deno Announcing Hono on JSR | Deno How We Made the Deno Language Server Ten Times Faster | Deno How the Guardian uses Deno to audit accessibility and performance across their 2.7 million articles | Deno Introducing More Flexible Domain Association for Deno Subhosting | Deno The stabilization process of the Standard Library has begun | Deno Deno 1.44: Private npm registries, improved Node.js compat, and performance boosts | Deno How we built a secure, performant, multi-tenant cloud platform to run untrusted code | Deno The Deno Standard Library is now available on JSR | Deno How to document your JavaScript package | Deno Your Low Code Solution Needs an Escape Hatch | Deno Deno 1.43: Improved Language Server performance | Deno How Slack used Deno to save months of engineering effort in launching their new platform | Deno JSR Is Not Another Package Manager | Deno Announcing the Hookdeck SDK on JSR | Deno Announcing the Neon Serverless Driver on JSR | Deno An intro to TSConfig for JavaScript Developers | Deno How we built JSR | Deno How Netlify used Deno Subhosting to build a successful edge functions product | Deno Introducing Simpler Project Creation in Deno Deploy | Deno Deno 1.42: Better dependency management with JSR | Deno Introducing deployctl, the command line interface for Deno Deploy | Deno Introducing JSR - the JavaScript Registry | Deno How to add Monaco to a Next.js app and securely run untrusted user code | Deno Survey Results and Roadmap | Deno Deno 1.41: smaller deno compile binaries | Deno
A Gentle Introduction to Islands | Deno
Andy Jiang · 2023-04-26 · via Deno

Modern JavaScript frameworks include a lot of JavaScript. That’s kinda obvious.

But most web sites don’t contain a lot of JavaScript.

Some do. If you’re building a dynamic interactive dashboard, JS your heart out. On the other end, documentation pages, blogs, static content sites, etc. require 0 JavaScript. This blog, for example, has no JS.

But then there’s this whole horde of websites in the middle that need some interactivity, but not a lot:

the goldilocks of javascript

These goldilocks “just enough” sites are a problem for frameworks: you can’t statically generate these pages, but bundling an entire framework and sending it over a network for one image carousel button seems overkill. What can we do for these sites?

Give them islands.

What are Islands?

Here’s how this looks on our merch site which is built with Fresh, our Deno-based web framework that uses islands:

islands of interactivity on our merch site

The main part of the page is static HTML: the header and footer, the headings, links, and text. None of these require interactivity so none use JavaScript. But three elements on this page do need to be interactive:

  • The ‘Add to Cart’ button
  • The image carousel
  • The cart button

These are the islands. Islands are isolated Preact components that are then hydrated on the client within a statically-rendered HTML page.

  • Isolated: these components are written and shipped independently of the rest of the page.
  • Preact: a tiny 3kb alternative to React, so even when Fresh is shipping islands it is still using the minimal amount of JS.
  • Hydration: how JavaScript gets added to a client-side page from a server-rendering.
  • Statically-rendered HTML page: The basic HTML with no JavaScript that is sent from the server to the client. If no islands were used on this page, only HTML would be sent.

The key part of that is the hydration. This is where JavaScript frameworks are struggling because it’s fundamental to how they work, but at the same time hydration is pure overhead.

JS frameworks are hydrating a page. Island frameworks are hydrating components.

The problem with hydration - “Hydrate level 4, please”

Why does so much JavaScript get sent without islands? It’s a function of the way modern ‘meta’ JavaScript frameworks work. You use the frameworks to both create your content and add interactivity to your pages, send them separately, then combine them in a technique called ‘Hydration’ in the browser.

In the beginning, these were separated. You had a server-side language producing the HTML (PHP, Django, then NodeJS) and client-side plugins providing interactivity (jQuery being the most prevalent). Then we got to the React SPA era and everything was client side. You shipped a bare bones HTML skeleton and the whole site, with content, data, and interactivity was generated on the client.

Then pages got big and SPAs got slow. SSR came back, but with the interactivity added without the plugins through the same codebase. You create your entire app in JS, then during the build step the interactivity and initial state of the app (the state of your components along with any data pulled server-side from APIs) are serialized and bundled into JS and JSON.

Whenever a page is requested, the HTML is sent along with the bundle of JS needed for interactivity and state. The client then ‘hydrates’ the JS, which means:

  • Traverse the entire DOM from a root node
  • For every node, attach an event listener if the element is interactive, add the initial state, and re-render. If the node isn’t supposed to be interactive (e.g. an h1), reuse the node from the original DOM and reconcile.

This way, the HTML is shown quickly so the user isn’t staring at a blank page and then the page becomes interactive once the JS has loaded

You can think about hydration like this:

Hydrate level four please! Back to the Future, Part 2

The build step takes all the juicy parts out of your application, leaving you with a dry husk. You can then ship that dry husk along with a separate gallon of water to be combined by your client’s Black & Decker hydrator browser. This gets you an edible pizza/usable site back (h/t to this SO answer for that analogy).

What’s the problem with this? Hydration treats the page as one single component. Hydration takes place top-down and traverses through the entire DOM to find the nodes it needs to hydrate. Even though you’re decomposing your app into components in development, that is thrown away and everything is bundled together and shipped.

These frameworks also ship framework-specific JavaScript. If we create a new next app and remove everything but an h1 on the index page, we still see JS being sent to the client, including a JS version of the h1, even when the build process says this page is statically generated:

Hello from Next

Code-splitting and progressive hydration are workarounds for this fundamental problem. They break up the initial bundle and the hydration into separate chunks and steps. This should make the page interactive quicker, as you can start hydrating from the first chunk before the rest is downloaded.

But you are still ultimately sending all this JavaScript to a client that may not be using it, and has to process it to find out if it’s going to use it.

How Islands work in Fresh

If we do something similar with Fresh, our Deno-based web framework we see zero JavaScript:

Hello from Fresh without JavaScript

Nothing on the page requires JS, so no JS was sent.

Now let’s add in some JavaScript in the shape of an island:

Hello from Fresh with islands

So we have three JavaScript files:

  • chunk-A2AFYW5X.js
  • island-counter.js
  • main.js

To show how those JS files appeared, here’s a timeline of what happens when a request is received:

Timeline of events from request

Note that this timeline is for the first request to a Fresh app. After the assets are cached, subsequent requests simply retrieve necessary scripts from the cache.

Let’s dig into the key steps that make islands work.

Check manifest from fresh.gen.ts for islands

The first step to locating any islands is to check the manifest from fresh.gen.ts. This is an auto-generated doc in your own app that lists the pages and islands in the app:



import config from "./deno.json" with { type: "json" };
import * as $0 from "./routes/index.tsx";
import * as $$0 from "./islands/Counter.tsx";

const manifest = {
  routes: {
    "./routes/index.tsx": $0,
  },
  islands: {
    "./islands/Counter.tsx": $$0,
  },
  baseUrl: import.meta.url,
  config,
};

export default manifest;

The Fresh framework processes the manifest into individual pages (not shown here) and components. Any islands are pushed into an islands array:




for (const [self, module] of Object.entries(manifest.islands)) {
  const url = new URL(self, baseUrl).href;
  if (typeof module.default !== "function") {
    throw new TypeError(
      `Islands must default export a component ('${self}').`,
    );
  }
  islands.push({ url, component: module.default });
}

Replace each island with a unique HTML comment during server-side rendering

During server rendering with render.ts, Preact creates a virtual DOM. As each virtual node is created, the options.vnode hook is called in Preact:



options.vnode = (vnode) => {
  assetHashingHook(vnode);
  const originalType = vnode.type as ComponentType<unknown>;
  if (typeof vnode.type === "function") {
    const island = ISLANDS.find((island) => island.component === originalType);
    if (island) {
      if (ignoreNext) {
        ignoreNext = false;
        return;
      }
      ENCOUNTERED_ISLANDS.add(island);
      vnode.type = (props) => {
        ignoreNext = true;
        const child = h(originalType, props);
        ISLAND_PROPS.push(props);
        return h(
          `!--frsh-${island.id}:${ISLAND_PROPS.length - 1}--`,
          null,
          child,
        );
      };
    }
  }
  if (originalHook) originalHook(vnode);
};

The function options.vnode can mutate the vnode before it’s rendered. Most vnodes (e.g., <div>) are rendered as expected. But if the vnode is both a function and has the same type of function as an element in the islands array (therefore, is the original node for the island), the vnode is wrapped in two HTML comments:



// the island vnode

</!--frsh-${island.id}:${ISLAND_PROPS.length - 1}-->

For the nerds out there, despite the fact that the closing comment, </!-- xxx --> is not valid HTML, the browser still parses and renders this correctly.

This info is also added to an ENCOUNTERED_ISLANDS set.

In our case, the title and the lemon image will render as expected, but once the vnode for Counter is created, the HTML comment !--frsh-counter:0-- is inserted.

(The reason why Fresh uses a comment (vs. a <div> or a custom element) is because introducing a new element can sometimes disturb the styling and layout on the page, leading to CLS issues.)

Dynamically generate hydration scripts

The next step is to generate hydration scripts based on the islands detected, based on all of the islands added to the set ENCOUNTERED_ISLANDS.

In render.ts, if ENCOUNTERED_ISLANDS is greater than 0, then we’ll add an import for the revive function from main.js to the hydration script that’ll be sent to the client:



if (ENCOUNTERED_ISLANDS.size > 0) {

  script += `import { revive } from "${bundleAssetUrl("/main.js")}";`;

Note that if ENCOUNTERED_ISLANDS is 0, the entire islands part is skipped and zero JavaScript is shipped to the client-side.

Then, the render function adds each island’s JavaScript (/island-${island.id}.js) to an array and its import line to script:



  let islandRegistry = "";
  for (const island of ENCOUNTERED_ISLANDS) {
    const url = bundleAssetUrl(`/island-${island.id}.js`);
    script += `import ${island.name} from "${url}";`;
    islandRegistry += `${island.id}:${island.name},`;
  }
  script += `revive({${islandRegistry}}, STATE[0]);`;
}

By the end of the render function, script, which is a string of import statements followed by the revive() function, is added to the body of the HTML. On top of that, the imports array with the URL path of each island’s JavaScript is rendered into an HTML string.

Here’s what the script string looks like when it’s loaded into the browser:

<script type="module">
  const STATE_COMPONENT = document.getElementById("__FRSH_STATE");
  const STATE = JSON.parse(STATE_COMPONENT?.textContent ?? "[[],[]]");
  import { revive } from "/_frsh/js/1fx0e17w05dg/main.js";
  import Counter from "/_frsh/js/1fx0e17w05dg/island-counter.js";
  revive({ counter: Counter }, STATE[0]);
</script>

When this script is loaded into the browser, it’ll execute the revive function from main.js to hydrate the Counter island.

The browser runs revive

The revive function is defined in main.js (which is the minified version of main.ts). It traverses a virtual dom searching for a regex match to identify any of the HTML comments Fresh inserted in the earlier step.



function revive(islands, props) {
  function walk(node) {
    let tag = node.nodeType === 8 &&
        (node.data.match(/^\s*frsh-(.*)\s*$/) || [])[1],
      endNode = null;
    if (tag) {
      let startNode = node,
        children = [],
        parent = node.parentNode;
      for (; (node = node.nextSibling) && node.nodeType !== 8;) {
        children.push(node);
      }
      startNode.parentNode.removeChild(startNode);
      let [id, n] = tag.split(":");
      re(
        ee(islands[id], props[Number(n)]),
        createRootFragment(parent, children),
      ), endNode = node;
    }
    let sib = node.nextSibling,
      fc = node.firstChild;
    endNode && endNode.parentNode?.removeChild(endNode),
      sib && walk(sib),
      fc && walk(fc);
  }
  walk(document.body);
}
var originalHook = d.vnode;
d.vnode = (vnode) => {
  assetHashingHook(vnode), originalHook && originalHook(vnode);
};
export { revive };

If we look in our index.html we’ll see we have that comment that would match that regex:

When revive finds a comment, it calls the createRootFragment with Preact’s render / h to render the component.

And now you have an island of interactivity ready to go on the client-side!

Islands in other frameworks

Fresh isn’t the only framework that uses islands. Astro also uses islands, though in a different configuration where you specify how you want each component to load its JavaScript. For instance, this component would load 0 JS:

But add a client directive and it’ll now load with JS:

<MyReactComponent client:load />

Other frameworks such as Marko use partial hydration. The difference between islands and partial hydration is subtle.

In islands, it’s explicit to the developer and framework which component will be hydrated and which won’t. In Fresh, for instance, the only components that ship JavaScript are the ones in the islands directory with CamelCase or kebab-case naming.

With partial hydration, components are written normally and the framework, during the build process, determines which JS should ship during the build process.

Other answers to this problem are React Server Components, which underpin NextJS’ new /app directory structure. This helps more clearly define work done on the server and work done on the client, though whether it reduces the amount of JS in the shipped bundle is still up for debate.

The most exciting development apart from islands is Quik’s resumability. They remove hydration completely and instead serialize the JavaScript within the HTML bundle. Once the HTML gets to the client the entire app is good to go, interactivity and all.

Islands in a stream

It might be possible to combine islands and resumability to ship little JS and remove hydration.

But there is more to islands than just smaller bundles. A huge benefit of the islands architecture is the mental model it makes you develop. You have to opt-in to JavaScript with islands. You can never ship JavaScript to the client by mistake. As a developer builds an app, every inclusion of interactivity and JavaScript is an explicit choice by the developer.

That way, it’s not the architecture and the framework that is shipping less JavaScript–it’s actually you, the developer.

Don’t miss any updates — follow us on Twitter.