セカンドパーティコンテンツをもつサードパーティスクリプトの作り方

KARTE Blocks(以下Blocks)では、Blocksを利用するサイトに1行の<script>タグを埋め込むことでサイト書き換えや効果計測をします。
Blocksでは、この<script>タグで埋め込むスクリプトファイルをbuilder.jsと呼んでいます。
この記事では、Blocksが扱うbuilder.jsというサードパーティスクリプトの仕組みについて紹介します。

  • builder.jsというセカンドパーティコンテンツをもつサードパーティスクリプトについて知る
  • builder.jsでは安全にサードパーティスクリプトを開発して、配布、読み込みしているのかを知る
  • サードパーティスクリプトの開発、テスト、デバッグ方法について知る

この記事は「KARTE Blocksリリースの裏側」の3日目の記事です。全10回の予定です。
これから毎日記事を更新していくため、更新をチェックしたい方は@KARTE_BlocksのTwitterアカウントをフォローしてください!

「KARTE Blocksリリースの裏側」の記事の一覧です。

  1. KARTE Blocksを支える技術
  2. インクリメンタルに新しい技術を取り入れる方法
  3. セカンドパーティコンテンツをもつサードパーティスクリプトの作り方 ← イマココ
  4. AWSが落ちてもGCPに逃がすことで落ちないシステムを作る技術
  5. ユーザーが自ら理解・学習するためのテックタッチなアプローチ
  6. 爆速で価値あるプロダクトをリリースするためのチームビルディング
  7. CSS in JSとしてVanilla-Extractを選んだ話と技術選定の記録の残し方
  8. 0→1のフェーズで複数のユーザー体験をつなぐUIデザインを考える|wagon|note
  9. リリース後に落ちないように、新規サービスで備えておいたこと
  10. KARTE Blocksにおけるポジショニングの考え方とその狙い
  11. 「KARTE Blocksリリースの裏側」の裏側 - 複数人で連載記事を書く方法

KARTE Blocksとは

KARTE Blocksは、サイトをブロックの集合体として捉えてブロックごとに更新・管理ができるウェブサービスです。
サイトの成長を実現するための分析、ABテスト、パーソナライズまで、ワンストップかつノーコードで誰でも簡単に行うことができるプロダクトです。
どのようなサイトでもタグを1行貼るだけでセットアップが完了し、すぐに利用できます。

今回の記事では、Blocksを利用するサイトに追加するタグの中身であるbuilder.jsについて解説していきます。

サードパーティスクリプトであるbuilder.js

Blocksでは管理画面で設定した内容をJavaScriptにビルドし、
このビルドしたファイルをBlocksのクライアントのサイトに埋め込むことで、ブロックごとの書き換えをしています。
このJavaScriptファイルをBlocksではbuilder.jsと呼んでいます。

builder.jsは、一般にサードパーティスクリプトと呼ばれるものです。
この場合のファーストパーティは、サイトを訪問したエンドユーザーです。
セカンドパーティとは、サイトの運営者であるBlocksのクライアントです。
ファーストパーティであるエンドユーザーにとって、Blocksは第三者にあたるためサードパーティとなります。

サードパーティスクリプトと呼ばれるものには、Google Analyticsのようなアクセス解析やTwitterのようなSNSボタンなどさまざまなものがあります。

Blocksのbuilder.jsの中身に含まれる設定は、Blocksのクライアント(セカンドパーティ)が作成したコンテンツです。
そのため、サードパーティスクリプトでありながら、中身はセカンドパーティのコンテンツを含むという性質を持っています。

サードパーティスクリプトの開発で難しい点は、サードパーティスクリプトはどのサイトで動くか事前に分からない点です。
実際にbuilder.jsのscript要素を埋め込むのはクライアントであるため、どのサイトにどのように埋め込むかを制限できません(もちろんガイドラインはあります)。
そのため、サードパーティスクリプトの開発者は、どのサイトでも安全に動くような仕組みや通常のウェブサイトよりも広い範囲をサポートする必要があります。

この記事では、サードパーティスクリプトとして動くbuilder.jsの仕組みや開発、またクライアントのサイトで安全に動かすためにやっている工夫について書いてきます。

builder.jsの配布と読み込み

Blocksでは、次のような1行のタグを読み込むだけで、管理画面で設定した内容をもとにサイトの表示を書き換えます。

<script src="https://cdn-blocks.karte.io/{PROJECT_ID}/builder.js"></script>

このタグを見ただけでいろいろなことがわかる人もいます。

まず、タグでのURLに{PROJECT_ID}という、プロジェクト(サイトと同じという認識で問題ありません)ごとに異なるスクリプトを読み込んでいることがわかります。
これは、プロジェクトごとに異なるコンテンツを含んだbuilder.jsを作成するため、サイトごとに読み込むスクリプトタグのURLが異なります。

次にCDNという単語がURLに入ってることからもわかりますが、Amazon CloudFrontGoogle Cloud CDNをCDNとして利用しています。
複数のCDNを使っているのは、大規模なCDN障害が発生した際に、CDNを切り替えるマルチクラウドの仕組みを持つためです。

最後に、script要素にはasync属性やdefer属性が入っていない同期スクリプトとして読み込んでいることがわかります。
これは、読み込み時にレンダリングブロックが発生するため、サードパーティスクリプトとしては避けることが多い書き方です。
しかし、先ほども書いたように、読み込んでいるのはセカンドパーティのコンテンツである点と違和感のない書き換えを実現するために、同期スクリプトを選択しています。

これらについても詳しく解説していきます。

IsolateされたBuilder Serverで、プロジェクトごとのbuilder.jsをビルドする

Blocksでは、プロジェクトごとに専用のbuilder.jsを生成して、S3やGCSといったストレージへアップロードしています。
CloudFrontやCloud CDNはそれぞれのストレージを参照できるため、builder.jsはCDN経由で配信できます。

このプロジェクトごとのbuilder.jsのビルドには、webpackを利用しています。
このときにbuilder.jsのビルドを行うのは、管理画面のAPIサーバーとは異なるBuilder Serverという別のプロセスです。

KARTE Blocksを支える技術という記事でも紹介していましたが、BlocksではKubernetesを利用しています。
そのため、具体的には管理画面とbuilder.jsをビルドBuilder ServerはPodとして分離されています。

builder-server

なぜビルドするサーバーを分けているかというと、セキュリティを担保するためです。

Blocksのクライアントが入力した設定情報とテンプレート的なコードを組み合わせてbuilder.jsを生成します。
この際にwebpackなどのビルドで使うツールにバグがあると、任意のコード実行につながる可能性もあります。
Isolateされた環境ならば、このような問題が仮に発生しても影響範囲が限られます。

また、APIを扱う処理とビルドを扱う処理は、CPUやメモリ消費の性質が異なるため、サーバーとして分離することが適切だと考え分けています。

現在はBuilder ServerからS3やGCSの特定のBucketへのアップロードが許可されていますが、
この権限もプロジェクトの固有のパスのみへのアップロードなどとより制限していくことが考えられます。

Cloudflareのcdnjsというサービスで同様の処理が実装されています。

統一されたJavaScriptファイルとプロジェクトごとのJavaScriptファイル

Blocksのbuilder.jsでは、builder.jsの中身はプロジェクトごとに異なります。
そのため、サイトごとに異なるJavaScriptファイルをロードしています。

別の方法として、読み込むJavaScriptファイルは同じにして、設定の定義だけを別にするような方式があります。
たとえば、次のように設定をグローバルに定義して、https://example.com/third.jsという全部のサイトで統一されたスクリプトを読み込むという形式です。

<script>
  window.exampleSharedConfig = {/* 設定 */};
<script>
<script src="https://example.com/third.js"></script>

この統一されたJavaScriptを読み込むメリットは、同じURLのJavaScriptファイルは一度どこかのサイトで読み込めばサイト間を超えてブラウザがキャッシュしていたため、キャッシュが効くという点になります。
しかしながら、現代ではプライバシーの懸念からこのキャッシュはサイトごとに分割されるようにHTTP cache partitionsという仕様が作られました。
たとえば、mysite.testから読み込むhttps://example.com/third.jsと、yousite.testから読み込むhttps://example.com/third.jsは別々のストレージにキャッシュされるため、共有されません。

そのため、現在は統一されたJavaScriptファイルを読み込むこととブラウザキャッシュの効きやすさは、関係なくなっています。

ただし、サイトごとに同じ処理である場合などは https://example.com/third.js のような統一されたJavaScriptファイルを読み込む方がシンプルです。
サイトごとにタグが異なる場合は、ドキュメントに次のようにサンプルのタグを書くと、そのままコピペして埋め込んでしまうような間違いも起きやすいです。

<script src="https://example.com/example_project_id/third.js"></script>

他にもJavaScriptファイルの読み方はさまざまな方法がありますが、どの読み込み方法を採用するかサービスの目的によって異なります。

Blocksでは、HTMLやJavaScriptになれてない人も利用できるサービスとなっています。
1行のタグを埋め込むだけで使えるシンプルさもあり、次のようなタグ一行でJavaScriptファイルを読み込む方式を採用しています。

<script src="https://cdn-blocks.karte.io/{PROJECT_ID}/builder.js"></script>

タグのコピペミスについては、管理画面上でプロジェクトごとbuilder.jsが入ったタグをコピできるようにすることでミスを減らしています。

builder.jsの書き換えの仕組み

Blocksのタグでbuilder.jsが読み込めたので、次はbuilder.jsがどのようにしてサイトの書き換えや出し分けをしているのかを見ていきます。

Blocksでのbuilder.jsは次のような流れで作成され、実際のウェブサイトから読み込まれます。

  1. 管理画面でブロックなどの設定内容を変更
  2. builderというサーバーが、設定内容をもとにプロジェクトごとのbuilder.jsをビルド
  3. 作成したbuilder.jsをS3とGCSにアップロードし、CDNで配信
  4. クライアントのサイトに埋め込まれた<script>タグでbuilder.jsを読み込む

図にすると次のような流れとなっています。

builder.jsが配信されるまでの流れ

実際の利用時の流れや解説については、ウェブサイトやガイドを見ることで解説されています。

そのため、ここではbuilder.jsが読み込まれた後に、どのようにbuilder.jsがサイトをブロックとして書き換えているのかを見ていきます。

ブロックの書き換えの基本的な仕組み

Blocksはブロック単位でサイトを書き換えます。
ブロックとは、特定のCSSセレクタと書き換え方法、書き換える条件をもったオブジェクトです。
ひとつのサイトには複数のブロックがあるため、builder.jsには複数のブロックをまとめた設定が埋め込まれています。

ものすごく単純なbuilder.jsの動きをコードで書いてみると次のようになります。
次のコードでは、builder.jsに埋め込まれたCONFIG(設定内容)のブロック情報を元に、サイトのDOMを書き換えています。

/* Blocksの設定の部分的な例。ビルド時にJavaScriptファイルに埋め込まれる。 */
const CONFIG = [{ selector: 'body > img', html: '<img src="https://via.placeholder.com/150/0000FF/FFFFFF?text=New%20Image"/>' }];

const rewrite = () => {
  CONFIG.forEach(({ selector, html }) => {
    const element = document.querySelector(selector);
    if (!element) return;
    element.outerHTML = html;
  });
};
const onReady = (callback) => {
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => {
      callback();
    });
  } else {
    callback();
  }
};
// main
onReady(() => {
  rewrite();
});

このままでは、ロード時に一度書き換えるだけとなるため、SPA(Single Page Application)などの動的にDOM構造が変わるサイトに対応できません。
そのため、builder.jsではMutationObserverを使い、元サイトのDOM構造の変更を検知して再書き換えをしています。

先ほどの疑似コードへMutationObserverを追加すると、次のようになります。ただし、このコードは問題があります。

/* Blocksの設定。ビルド時にJavaScriptファイルに埋め込まれる。 */
const CONFIG = [{ id: "xxxx-xxxx-xxxx-xxxx", selector: 'body > img', html: '<img src="https://via.placeholder.com/150/0000FF/FFFFFF?text=New%20Image"/>' }];

const rewrite = () => {
  CONFIG.forEach(({ id, selector, html }) => {
    if (!element){
      return;
    }
    element.outerHTML = html;
  });
};
const observeDOMChange = (callback) => {
  const observer = new MutationObserver(() => {
    callback();
  });
  // DOMの変更を検知する
  observer.observe(document.documentElement, {
    attributes: true,
    childList: true,
    characterData: true,
    subtree: true,
    attributeOldValue: true,
  });
}
const onReady = (callback) => {
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => {
      callback();
      observeDOMChange(() => callback());
    });
  } else {
    callback();
    observeDOMChange(() => callback());
  }
};
// main
onReady(() => {
  rewrite();
});

この疑似コードの問題は無限ループしてしまうことです。
MutationObserverは、DOM変更に反応するため、自分自身の書き換えに対して再書き換えが走ってしまう無限ループが発生してしまいます。

これを防止するには、すでに書き換えされているブロックは再書き換えしないという方法が考えられます。
これは、一度書き換えたブロックの要素には書き換えたことを示すdata属性を割り当てておき、そのdata属性が存在する場合は書き換えないことで実現できます。

先ほどの疑似コードに、自分自身の書き換えに対する無限ループの対策を入れてみます。
書き換えた要素に対してdata-krt-blocks-id属性をつけるために、template要素を作って複製してから属性を足すなど少し複雑になっていきます。

/* Blocksの設定。ビルド時にJavaScriptファイルに埋め込まれる。 */
const CONFIG = [{ id: "xxxx-xxxx-xxxx-xxxx", selector: 'body > img', html: '<img src="https://via.placeholder.com/150/0000FF/FFFFFF?text=New%20Image"/>' }];

const rewrite = () => {
  CONFIG.forEach(({ id, selector, html }) => {
    const isRewritten = document.querySelector(`[data-krt-blocks-id="${id}"]`);
    if (isRewritten) {
      return;
    }
    const element = document.querySelector(selector);
    if (!element){
      return;
    }
    // template
    const template = document.createElement("template");
    template.innerHTML = html;
    const templateContent = template.content.firstElementChild;
    templateContent.setAttribute("data-krt-blocks-id", id);
    // 実際の書き換え
    element.outerHTML = templateContent.outerHTML;
  });
};
const observeDOMChange = (callback) => {
  const observer = new MutationObserver(() => {
    callback();
  });
  // DOMの変更を検知する
  observer.observe(document.documentElement, {
    attributes: true,
    childList: true,
    characterData: true,
    subtree: true,
    attributeOldValue: true,
  });
}
const onReady = (callback) => {
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => {
      callback();
      observeDOMChange(() => callback());
    });
  } else {
    callback();
    observeDOMChange(() => callback());
  }
};
// main
onReady(() => {
  rewrite();
});

このようにbuilder.jsの書き換えの仕組みは、静的なサイトと動的なサイトどちらにも対応できるようになっています。
また、builder.jsでは単純な書き換え以外にも、訪問したユーザーによってブロックを出し分けるパーソナライズや書き換えた効果の計測といった処理も行います。

しかし、実際に動くコードは、さまざまなサイトでのエッジケースにも対応する必要があります。
なぜなら、builder.jsのサードパーティスクリプトはどのようなサイトに埋め込まれるかわからないためです。

builder.jsがあらゆるサイトで安全に動くための仕組みについて

builder.jsの基本的な仕組みは、先ほどの疑似コードと似ています。
これは、意図的に単純な仕組みにすることで、多種多様なサイトに対して対応できる余地を残し、メンテナンスしやすくするためです。
サイトごとの細かいエッジケースに対応していくコードを増やしていくと、コードが複雑になりリグレッションを起こしやすくなります。

基本的な仕組みは同じですが、実際のbuilder.jsではRedner Moduleという抽象化されたレンダリングオブジェクトがあります。
このレンダリングオブジェクトに対するレンダリング方法を実装していくという形でかなりモジュール化されています。
これは、DOM操作は副作用のカタマリとなりやすいので、できるだけ一貫性のある結果となることを目的にしています。

ReactやVueなど現代のUIフレームワークは内部的には似たような構造を持っています。
サードパーティスクリプトであるbuilder.jsはファイルサイズなどを理由に外部ライブラリを取り入れるのは難しいため、内部的に持っています。

複雑になりすぎないように気をつけていますが、現実にはさまざまなウェブサイトがあり、どこに埋め込まれるか分からないサードパーティスクリプトはこの他にも対応する必要があります。

いくつか実際に含まれている、あらゆるサイトで安全に動くための仕組みをみていきます。

2重ロードの防止

最初に書いていたように、Blocksのタグであるbuilder.jsは次のように1行のタグで1つ読み込むだけです。

<script src="https://cdn-blocks.karte.io/{PROJECT_ID}/builder.js"></script>

しかし、サーバーの仕組みや間違いなどから同じタグを複数読み込んでしまうようなケースがあります。
たとえば、次のように2箇所で同じタグを読み込んでしまうと、二重に書き換えが行われたり、MutationObserverによって無限ループする可能性があります。

<!DOCTYPE html>
<html>
<head>
  <script src="https://cdn-blocks.karte.io/{PROJECT_ID}/builder.js"></script>
</head>
<body>
  <script src="https://cdn-blocks.karte.io/{PROJECT_ID}/builder.js"></script>
</body>
</html>

そのため、builder.jsでは2つのタグがあったも、片方だけが動くに二重実行のチェックを入れています。
これは、ものすごく単純でwindowに対してグローバルなフラグをおいて、すでに実行されているなら何もしないようにするだけです。

if(window.__KARTE_BLOCKS_IS_EXECUTED_BUILDERJS){
  return; // 既に実行済みなので何もしない
}
window.__KARTE_BLOCKS_IS_EXECUTED_BUILDERJS = true;

MutationObserverを使った書き換えの無限ループ対策

先ほどもMutationObserverの無限ループへの対策として、書き換え済みのブロックは再書き換えしないようにしている話をしました。
他にもMutationObserverが反応することで、書き換えに対して書き換えをしてしまい、無限ループが起こるケースもあります。

たとえば、サイト側でMutationObserverを使って、要素の書き換えているケースなどです。
この場合は、builder.js側とサイト側どちらもMutationObserverで要素の変更を監視して、書き換えているため無限に書き換えが発生してしまいます。

この無限ループを防ぐために、builder.jsではMutationObserverを使う際にRateLimitを実装しています。
一定回数または一定時間繰り返し書き換えを繰り返している場合には、無限ループが発生していると認識して、書き換えをしないようにしています。

チラツキの防止

builder.jsはブロック単位でサイト側の要素を書き換えます。
このときにも、サイト側の元の要素が一瞬でも見えてから、書き換えが発生するとチラツキが起きてしまいます。

これを防止するために、builder.jsでは書き換え前に、ブロックごとに要素の透明化の処理をしています。
ブロックの要素が透明な状態で書き換えて、書き換えが完了したら透明化を解除しています。

元要素を透明化 → 書き換え → 透明化解除

これによって書き換え時に発生するチラツキが起きにくくなるようにしています。

また、この書き換えはページロード時にも行われています。
これは、builder.jsが同期スクリプトを選んだ理由でもあります。

ブラウザの互換性の静的チェック

サードパーティスクリプトがサポートするブラウザの範囲は自然と広くなります。
なぜなら、できるだけサポート範囲を増やそうとすると、タグが埋め込まれるサイトのユーザーがサポート対象となるためです。

Blocksのサポートブラウザについては次のドキュメントを参照してください。

これらのサポートブラウザでの動作を確認するために、Unit TestやE2E Testも行っています。
しかし、DOM APIを多く扱うbuilder.jsでは、コードを書いている最中にサポートブラウザで使えないAPIは書けないことが理想です。

これを実現するために、ESLint + eslint-plugin-compat/eslint-plugin-typescript-compatを利用しています。

ESLintはLintツールですが、eslint-plugin-compatを使いサポートブラウザで利用できるDOM APIを静的にチェックできます。
しかし、eslint-plugin-compatArray.prototype.includesのようなメソッド名は判定できません。
なぜなら、JavaScriptではa.includes(1)と書かれていた場合にincludesメソッドはArrayのものなのか、独自実装なのかを判別できないためです。

この欠点をeslint-plugin-typescript-compatで補完しています。
eslint-plugin-typescript-compatはTyepScriptの型情報を使い、a.includes(1)aArray型かどうかをチェックすることで判別します。

ESLintのような静的なツールを使うことで、実際に実行する前に利用できないAPIを認識できます。

マルチクラウドCDNの利用

Blocksではbuilder.jsの配信にAmazon CloudFrontGoogle Cloud CDNを利用しています。

基本的には片方のCDNを利用していて、CDNに障害が発生した際に、もう片方のCDNへと切り替えられるようにマルチクラウド構成となっています。

このマルチクラウドの仕組みについては、明日公開の「AWSが落ちてもGCPに逃がすことで落ちないシステムを作る技術」の記事を参照してください。

builder.jsの開発について

Blocksでのbuilder.jsの開発環境についても簡単に見ていきます。

サードパーティスクリプトと言っても基本的な開発ツールは、ウェブサービスのフロントエンドとあまり変わりません。
一方で、デバッグなどはサードパーティスクリプト特有の手法が必要になってきます。

Unitテスト

UnitテストにはJestjsdomを利用しています。

jsdomではなくリアルなDOMを使うテストの方がよいのですが、
builder.jsが扱うDOM APIは基本的なものが中心となっているためjsdomでも問題ありませんでした。

実際には、Unitテストレベルではブラウザ間の差異がでることはかなり少ないです(APIが実装されているかどうかの違いはあります)。
Unitテストレベルはweb-platform-testsに書かれていて、各ブラウザはこのテストを利用しているため、普通に書く範囲の際は起きにくくなっています(マイナーなAPIなどのエッジケースはあります)。

そのため、Blocksではブラウザ差異については別のテストによって確認しています。

E2Eテスト

実際にシステムでbuilder.jsをビルドして配信して、書き換えが成功したかなどのテストを各ブラウザで実行できます。

システム自体はKubernetesでのテスト環境を使い、ブラウザはBrowserStackを使っています。
このE2Eテストで、サポート対象のブラウザでの動作を確認しています。

Layoutテスト

これは特定のフレームワークではなく、ブラウザを使ったテストをより実際のサイトに近づけた状態でテストする独自の仕組みです。

基本的な概念は、先ほどもでてきたweb-platform-testsのTest Layoutと同様の仕組みです。

Layoutテストは次のようなディレクトリ構造になっていて、server.tsでテスト用のサーバーを起動します。
このサーバーを起動すると https://localhost.test/intersection-observer-test のように各ディレクトリをテストケースとして扱えます。
たとえば、intersection-observer-testに書かれているsetting.jsonからbuilder.jsをビルドして、index.htmlではそのビルドしたbuilder.jsを読み込んだ状態でテストできます。

LayoutTests/
├─ server.ts
└─ intersection-observer-test
    ├── index.html
    └── settings.json

このindex.htmlは任意のHTMLを書くことができ、<script src="{builderjs}"></script>という形でビルドされたbuilder.jsを読み込めます。

つまり、Layoutテストは、特定のウェブページや特定の状態を再現した状態で、その結果がどうなるかをテストする仕組みです。
サードパーティスクリプトは、あらゆるサイトで動かす可能性があるので、特定のサイトではうまく動かないといったエッジケースは必ずでてきます。
これらのエッジケースを再現した状態でテストすることが目的です。

実際のサイトで確認するのは正しいですが、実際のサイトは複雑で原因が特定できない場合もあります。
そのため、最小構成の再現するテストをLayoutテストとして書いて、builder.jsの動作を確認しています。

実際のサイトでデバッグ

テストについて紹介しましたが、実際のサイトで確認するのが一番確実です。

しかし、自分たちで管理しているならデバッグ用のビルドを読み込めますが、クライアントのサイトでは直接はできません。
このような場合にはリライティングプロキシと呼ばれるようなツールをつかうことで、読み込むbuilder.jsをデバッグビルドに置き換えます。

リライティングプロキシはFiddler/Charles/Proxymanなどが有名で、Remote Map/Local Map機能とも呼ばれます。
また、ChromeやSafariの開発者機能にも同様の特定のURLとローカルファイルを置換する仕組みが実装されています。

builder.jsはプロジェクトごとのブロック設定とbuilder.jsのコードからビルドして作成します。
このローカルで作成したデバッグビルドと本番のbuilder.jsをProxyで置換することで、本番のサイトでのデバッグが可能です。

Proxyソフトウェアは、ブラウザの開発者ツールがあまり良くできていないブラウザやモバイルでも利用できるので、この手法は古典的ですが有用です。
また、開発者ツールを使うと開発者ツールが観測することで挙動やパフォーマンス特性が変わるため、Proxyソフトウェアを使ったデバッグは必要なケースがあります。

builder.jsとパフォーマンス

サードパーティスクリプトはファイルサイズが小さいほどよいです。

builder.jsの場合はセカンドパーティのコンテンツが含まれているため、単純なSDKライクなサードパーティスクリプトよりはファイルサイズが大きくなりやすいです。それでも、開発時からファイルサイズを意識する仕組みを取り入れています。

Size Limitでのファイルサイズチェック

Size Limitを使うことで、ファイルサイズが一定以上になったかをCIでチェックできます。

Blocksでは、size-limit-actionのGitHub Actionsを使い、Pull Requestごとにファイルサイズの変化を表示しています。
また、同時に特定のファイルサイズ以上になったらCIを落とすようにしています。

これは、意図しないライブラリが含まれて急激にサイズが大きくなってしまうような問題を防ぐためです。

CDNでの配信設定

builder.jsはCDNで配信しているので、配信を最適化するためのCDNの設定をしています。

builder.jsはただの静的なファイルであるため、Cache-Controlの設定、ファイルサイズを小さくするためにgzipbr(Brotil)での圧縮といった基本的な設定です。
CDNについては次の書籍が詳しいです。

同期スクリプトのトレードオフ

現時点のbuilder.jsは<script src="{builderjs}"></script>のように同期ロードするスクリプトタグで読み込んでいます。
これは、書き換え前に元要素が見えてしまうチラツキの問題を避けるのが主な目的です。

<script src="{builderjs}" async></script>のように非同期ロードで読み込むと、
元々のHTMLにかかれている要素が、読み込みに一瞬見えてしまうチラツキの問題がおきてしまいます。

一方で同期ロードを利用すると、ロード時に表示の遅延が発生してしまうトレードオフが存在します。
ドキュメントでも解説していますが、ロード時以外は非同期処理を基本としていて、他の処理を妨げないようにしています(同期ロードはちらつきの防止を目的としているため)。

しかし、このトレードオフのデメリットはできるだけ小さくする必要があります。

現在もトレードオフのデメリットを小さくするための改善を続けています。
たとえば、プロジェクト(サイト)ごとのbuilder.jsをビルドできることを生かして、サイトのサポートブラウザに合わせたビルド設定をして不要な依存を削りファイルサイズを小さくすること。
また、同期スクリプトで同期処理が必要なのはチラツキの防止をするための透明化処理のみなので、他の書き換えに必要なデータはbuilder.jsに含めないで、非同期に取得するよう方法などが考えられます。
これらについても検証して体験が良くなるかを検証しながら、実装しています。

おわりに

Blocksが提供するサードパーティスクリプトであるbuilder.jsの仕組みについて紹介しました。

builder.jsはサードパーティスクリプトではありますが、基本的な開発方法についてはフロントエンドの開発と同じです。
一方で、多種多様なサイトでも安全に動く仕組みやデバッグ方法に一部特殊な状況を想定する必要があります。

また、多種多様なサイトで動くため、テストやパフォーマンスもコードを書くときに意識する必要があります。
サードパーティスクリプトは、デプロイしたときに影響範囲が広くなりやすいため、事前にできることは事前にしておく必要があります。

この記事は「KARTE Blocksリリースの裏側」という連載の3日目の記事です。全10回の予定です。
これから毎日記事を更新していくため、更新をチェックしたい方は@KARTE_BlocksのTwitterアカウントをフォローしてください!

最後に、KARTE Blocks自体の開発に興味がある!というエンジニア(インターンも!)を募集しています!
詳しくは弊社エンジニア採用ページ採用スライドをご覧ください!