くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

satori-htmlで画像を埋め込むとかなり遅いので工夫してみた

以前書いたsatori/sharp/satori-htmlでOG画像生成を使ってたら、
やたらsatori-htmlの処理に時間がかかるので、
いろいろ調べてみたときの備忘録(*´ω`*)

使ってるのは前回と同じ、この3つ

遅かったときのコード

最初はこんな感じで、
いくつかの画像をBase64で埋め込む感じに。

import satori from "satori";
import { html } from "satori-html";
import sharp from "sharp";
import fs from "fs";

// 画像を読み込む処理
async function readImageBase64(filePath: string) {
  // 画像ファイルの読み込み
  const imageBuffer = await fs.readFileSync(filePath);
  // 画像ファイルをBase64形式にエンコード
  const imageBase64 = Buffer.from(imageBuffer).toString("base64");
  return `data:image/png;base64,${imageBase64}`;
}

// 画像の準備
const image1 = await readImageBase64("<image_path1>");
const image2 = await readImageBase64("<image_path2>");
const image3 = await readImageBase64("<image_path3>");

// satori-htmlで文字列をVNodeに変換
const vnode = html(`
  <div style="display: flex;">
    <img src="${image1}" />
    <img src="${image2}" />
    <img src="${image3}" />
  </div>
`);

// satoriでVNodeをSVGに変換
const svg = await satori(
  vnode,
  { width: 1200, height: 630, fonts: [] },
);

// sharpでSVGからPNGに変換
const png = await sharp(Buffer.from(svg)).png().toBuffer();

ただ、const vnode = html()の部分がかなり遅く。。

satori-htmlはなにをしているのか

satori-htmlのコードを見てみると1ファイルのみ。

やっていることとしては、

  • 文字列をultrahtmlでパース
  • パースしたHTMLのASTを探索してVNodeに変換

VNodeはsatoriが受け取れる形式。

interface VNode {
  type: string;
  props: {
    style?: Record<string, any>;
    children?: string | VNode | VNode[];
    [prop: string]: any;
  };
}

なぜ遅かったのか

Base64形式の画像の文字数は大量で、
大量の文字列をパースしているで時間がかかるよう。。
パース自体も重い処理なので、それはそう。。

改善方法

satori-htmlに渡す文字列を少なくすればよいので、
satori-htmlが返すVNodeに対して置換する感じにしてみた。

// ...略
// 画像の準備
const image1 = await readImageBase64("<image_path1>");
const image2 = await readImageBase64("<image_path2>");
const image3 = await readImageBase64("<image_path3>");

// 対応するキーワードとBase64形式画像のマップ
const imageMap = {
  "@@image1@@": image1,
  "@@image2@@": image2,
  "@@image3@@": image3,
};

// satori-htmlで文字列をVNodeに変換
const vnode = htmlWithAssets(`
  <div style="display: flex;">
    <img src="@@image1@@" />
    <img src="@@image2@@" />
    <img src="@@image3@@" />
  </div>
`, imageMap);

htmlWithAssetsの中身はこんな感じ。

import * as flat from "flat";
import { html } from "satori-html";

function htmlWithAssets(
  template: string, 
  assetMap: { [key: string]: string; },
) {
  // satori-htmlでVNode(JavaScript Object)に変換
  const vnode = html(template);
  
  // VNodeのネストオブジェクトをflatにする
  const flatVNode = flat.flatten(vnode);
  
  // 各key-valueごとにvalueの中身を見て置換
  const assetKeys = Object.keys(assetMap);
  const replacedEntries = Object.entries(flatVNode).map(v => {
    if (typeof v[1] == "string") {
      // valueにassetMapのキーを探す
      const foundKey = assetKeys.find((x) => v[1].includes(x));
      if (foundKey != null) {
        // 見つけたassetMapのキーを使って置換
        return [v[0], v[1].replace(foundKey, assetMap[foundKey])];
      }
    }
    // 見つからなければそのまま返す
    return v;
  });
  
  // Entiresをもとに戻す
  const applyedObject = Object.fromEntries(replacedEntries);

  // flattenしたObjectをもとに戻す
  return flat.unflatten(applyedObject);
}

flatten/unflatteには、このライブラリを利用

background-image: url("@@image1@@");とかもあるので、
単にvalueの一致だとダメなので、includesを使って探す感じにしてる。


以上!! これで、10分以上かかっても生成できなかったので、
数秒で表示されるようになった(*´ω`*)