Takumi

Image loading & emoji

Load remote, pre-fetched, and raw-pixel images, render emoji, and tune fetch limits and caches in Takumi.

Takumi fetches the remote images your markup references, accepts images you fetch or decode yourself, and turns emoji into images. Fetch limits, retries, caches, and decode limits come after these, at the end of the page.

External images

render() and ImageResponse fetch every remote http:// or https:// URL the tree references:

  • an <img> src
  • background-image, mask-image, and list-style-image in a node's style or tw

Takumi does not fetch URLs inside css or a <style> tag. Pass those images as pre-fetched images.

When user input can reach the markup, set allowUrl, as URLs from user input describes.

Share a fetchCache across renders to reuse downloaded bytes and pending requests:

Share downloaded image bytes
import {  } from "takumi-js";

const  = < ="https://example.com/image.png" />;

const  = new <string, <>>();

const  = await (, {
  : {
    , 
  },
});

fetchCache takes a Map<string, Promise<ArrayBuffer>> or any object with the same get, set, and delete methods. Values are promises, not raw buffers.

A plain Map never evicts entries. If each render uses a different URL, pass a bounded cache such as an LRU, or leave fetchCache out.

Pre-fetched images

Give images a key so the renderer never fetches them. Use the key in any src or CSS url().

Load named image sources
import {  } from "takumi-js/response";

export function (: ) {
  return new (< />, {
    : [
      {
        : "my-logo",
        : () => (new ("/logo.png", .)).(() => .()),
      },
      {
        : "background",
        : () => (new ("/background.png", .)).(() => .()),
      },
    ],
  });
}

function () {
  return (
    <
      ={{
        : "url(background)",
      }}
    >
      < ="my-logo" />
    </>
  );
}

data is raw bytes or a function that returns them. Takumi skips fetching any remote URL that already has an entry.

Raw pixels

Pass raw RGBA pixels instead of encoding a PNG first, and Takumi skips decoding. The Bitmap helper renders a row-major buffer of width * height * 4 bytes as an <img>. This example fills a 64 × 64 image with opaque blue:

Render raw RGBA pixels
import {  } from "takumi-js";
import {  } from "takumi-js/helpers/jsx";

const  = new (64 * 64 * 4);

for (let  = 0;  < .;  += 4) {
  .([59, 130, 246, 255], );
}

const  = await (< ={64} ={64} ={} />, {
  : 64,
  : 64,
});

Set premultiplied when the bytes already have premultiplied alpha. Takumi then uses them as they are. Outside JSX, an image node's src takes the same { width, height, data, premultiplied? } object.

Emoji

takumi-js turns emoji into images by default. It splits each emoji out of the text and fetches its SVG from a CDN, using Twemoji unless you pick another set.

Dynamic fetching

Set emoji to choose the set. render, renderSvg, renderAnimation, measure, and ImageResponse all take it, and the CDN values match satori's. Each emoji's SVG comes from jsDelivr, at the version the table lists:

ValueEmoji setFetched from
twemoji (default)Twemojijdecked/twemoji 17.0.2
blobmojiBlobmoji@svgmoji/blob 2.0.0
notoNoto Emojigooglefonts/noto-emoji v2.051
openmojiOpenMoji@svgmoji/openmoji 2.0.0
fluentFluent Emoji, colorshuding/fluentui-emoji-unicode, latest
fluentFlatFluent Emoji, flatshuding/fluentui-emoji-unicode, latest
from-fontGlyphs from a color font you loadNo fetch. See COLR / bitmap fonts
Fetch emoji images
import {  } from "takumi-js/response";

export function () {
  return new (< ="flex justify-center items-center text-3xl">Hello 👋😁</>, {
    : "noto", 
  });
}

Emoji detection follows the Unicode presentation rules. A pictograph that defaults to text presentation, such as ‼ or ▶, stays text and renders with your fonts. Add U+FE0F after it to force the emoji image. Add U+FE0E after an emoji to keep the text glyph.

Manual extraction

The extractEmojis helper does the splitting. It replaces each emoji with an image node that points at the CDN. Call it yourself to fetch emoji with your other images on your own renderer.

Extract emoji before rendering
import {  } from "takumi-js/helpers/emoji";
import {  } from "takumi-js/helpers/jsx";
import {  } from "takumi-js/helpers";
import {  } from "takumi-js/node";

let {  } = await (< ="flex justify-center items-center text-3xl">Hello 👋😁</>);
 = (, "twemoji");

const  = await ({  });

const  = new ();

const  = await .(, {
  ,
});

prepareImages fetches the remote URLs missing from sources. It returns the supplied sources and the fetched entries together. It takes the same fetch cache and limits.

COLR / bitmap fonts

Takumi draws emoji from color fonts, either COLR fonts such as Twemoji-COLR or bitmap fonts such as Noto Color Emoji. A COLR file is much smaller than a bitmap font. The glyphs come from the loaded font, so no emoji request goes out.

To use a color font, do both of these:

  • Register the font through fonts.
  • Set emoji: "from-font".

Variation selectors take priority over font order. A code point followed by U+FE0F renders as a color emoji, even when a text font comes first in the stack. A code point followed by U+FE0E renders as a monochrome glyph, even when a color font comes first. Bare code points follow stack order, as in browsers.

Fetch limits

Limit image fetches
import {  } from "takumi-js";

const  = < ="https://example.com/image.png" />;

const  = await (, {
  : {
    : 3000,
    : 4 * 1024 * 1024,
    : () => new (). === "example.com",
  },
});

images, googleFonts, and fontFromUrl take these options. A custom data loader makes its own requests, so they do not apply to it.

OptionDefaultEffect
timeout30000 msDeadline for each request, including retry delays. 0 or negative disables it.
signalnoneCancels a request and any pending retry.
maxBytes32 MiBRejects larger bodies, by content-length and by streamed bytes.
allowUrlallow allRejects a URL before fetching it, redirect destinations included.
fetchglobalThis.fetchReplaces the fetch implementation.

Retries

GET and HEAD requests make up to three attempts. A request retries when fetch throws a TypeError (such as a connection error) or a TimeoutError, and on HTTP 408, 429, 500, 502, 503, and 504. Takumi waits 100 ms before the first retry and 200 ms before the second. A Retry-After header can extend a wait to at most one second. If it asks for a longer wait, Takumi returns the error without retrying.

Retries also stop when any of these happens:

  • the caller's signal aborts
  • the deadline passes
  • any other HTTP error
  • a failure while reading the response body

A custom fetch must honor the abort signal it receives.

URLs from user input

Takumi fetches these URLs from your server. Set allowUrl whenever user input can reach the markup.

  • allowUrl checks each redirect destination, up to five hops. Each hop shares the same deadline and gets its own attempts.
  • A fetchCache hit checks only the requested URL, because cached bytes keep no redirect history. Do not share one fetchCache across different URL policies.
  • allowUrl sees URLs, not IP addresses. To filter by resolved address, pass a custom fetch.

Decode caching

Takumi caches each decoded image, so repeated content decodes once. There are two caches:

CacheStoresKeyed bySet with
Byte cacheFetched bytesURLimages.fetchCache
Decode cacheDecoded pixelsHash of image bytesper-image cache / images.cache

A data: URI in an <img> or a CSS url() uses the same decode cache, keyed by the bytes it holds. A template that inlines its logo decodes it once. data: URIs always cache in auto mode, because images.cache applies only to sources and to URLs Takumi fetches.

The decode cache belongs to the renderer and has a byte budget, 64 MiB by default. Each entry counts its decoded size. When the cache is full, it evicts the least recently used entry. SVG sources, their rasterized pixels, and parsed stylesheets share the same budget. A workload with a different image every render does not grow memory. It misses the cache and decodes each time.

Set the budget when you create the renderer. 0 turns caching off.

Set the decode cache budget
import {  } from "takumi-js/node";

const  = new ({ : 64 * 1024 * 1024 });

The per-image cache mode takes two values.

  • auto is the default and caches the decoded image within the budget.
  • none reads the cache but never adds to it. Use it for a one-off image, so it does not push out images worth keeping.
Skip caching one image
import {  } from "takumi-js/response";

export function (: ) {
  return new (< ="banner" />, {
    : [
      {
        : "banner",
        : () => (new ("/banner.png", .)).(() => .()),
        : "none", 
      },
    ],
  });
}

To set the mode for every image in a render, pass images as an object instead of an array. This includes images Takumi fetches itself. A source's own cache still wins. For a route that renders a different image on every request, cache: "none" keeps one-off decodes out of the cache:

Skip caching all images
import {  } from "takumi-js/response";

export function (: ) {
  return new (< />, {
    : {
      : "none", 
      : [
        {
          : "logo",
          : () => (new ("/logo.png", .)).(() => .()),
        },
      ],
    },
  });
}

function () {
  return < ="logo" />;
}

Decode limits

Takumi rejects oversized images when it decodes them. The limits protect servers from decode bombs, which are small files that declare huge dimensions to force huge allocations.

InputLimit
Image8192 × 8192 pixels
Animated GIF, APNG, or WebP4 × 8192 × 8192 pixels summed over all frames. Frames past 1024 are dropped.

Last updated on