Next (または任意の SSR フレームワーク) で Web コンポーネントを使用する

私の中で 以前の投稿 Shoelace は、美しく、アクセスしやすく、おそらく予想外に、UX コンポーネントの完全なスイートを備えたコンポーネント ライブラリです。 Webコンポーネント. これは、どの JavaScript フレームワークでも使用できることを意味します。 React の Web コンポーネントの相互運用性は、現時点では理想的とは言えませんが、 回避策があります.

しかし、Web コンポーネントの深刻な欠点の XNUMX つは、現在サーバー側レンダリング (SSR) がサポートされていないことです。 開発中の Declarative Shadow DOM (DSD) と呼ばれるものがありますが、現在のサポートはごくわずかであり、実際には、DSD 用の特別なマークアップを発行するには Web サーバーからの同意が必要です。 現在、次の作業が行われています Next.js 楽しみにしています。 しかし、この投稿では、Next.js などの任意の SSR フレームワークから Web コンポーネントを管理する方法を見ていきます。 今日.

かなりの量の手作業を行うことになり、 わずかに その過程でページの起動パフォーマンスが低下します。 次に、これらのパフォーマンス コストを最小限に抑える方法を見ていきます。 ただし、間違いはありません。このソリューションにはトレードオフがないわけではないため、別の方法を期待しないでください。 常に測定してプロファイリングします。

問題

本題に入る前に、少し時間を取って実際に問題を説明しましょう。 Web コンポーネントがサーバー側のレンダリングでうまく動作しないのはなぜですか?

Next.js のようなアプリケーション フレームワークは、React コードを受け取り、API を介して実行して、本質的に「文字列化」します。つまり、コンポーネントをプレーンな HTML に変換します。 したがって、React コンポーネント ツリーは Web アプリをホストするサーバー上でレンダリングされ、その HTML は Web アプリの残りの HTML ドキュメントと共にユーザーのブラウザーに送信されます。 この HTML に加えて、 React をロードするタグと、すべての React コンポーネントのコード。 ブラウザがこれらを処理するとき タグを使用すると、React はコンポーネント ツリーを再レンダリングし、送信された SSR の HTML と照合します。 この時点で、すべてのエフェクトが実行を開始し、イベント ハンドラーが接続され、状態に実際に状態が含まれます。 この時点で、Web アプリは次のようになります。 相互作用的. クライアント上でコンポーネント ツリーを再処理し、すべてを接続するプロセスが呼び出されます。 水分補給.

では、これは Web コンポーネントとどのような関係があるのでしょうか? さて、何かをレンダリングするときは、同じ靴ひもを言ってください 訪問したコンポーネント 前回:


   General 
   Custom 
   Advanced 
   Disabled 

  This is the general tab panel.
  This is the custom tab panel.
  This is the advanced tab panel.
  This is a disabled tab panel.

…反応する(または正直に どれか JavaScript フレームワーク) はそれらのタグを見て、単純にそれらを渡します。 React (または Svelte、または Solid) は、これらのタグを適切にフォーマットされたタブに変換する責任を負いません。 そのためのコードは、それらの Web コンポーネントを定義するコードの中に隠されています。 私たちの場合、そのコードは Shoelace ライブラリにありますが、コードはどこにでも置くことができます。 重要なのは コードが実行されるとき.

通常、これらの Web コンポーネントを登録するコードは、JavaScript を介してアプリケーションの通常のコードに取り込まれます。 import. つまり、このコードは JavaScript バンドルに取り込まれ、ハイドレーション中に実行されます。つまり、ユーザーが最初に SSR の HTML を表示してからハイドレーションが発生するまでの間、これらのタブ (または Web コンポーネント) は正しいコンテンツをレンダリングしません。 . その後、ハイドレーションが発生すると、適切なコンテンツが表示され、これらの Web コンポーネントの周囲のコンテンツが移動し、適切にフォーマットされたコンテンツに収まる可能性があります。 これは、 スタイルのないコンテンツのフラッシュ、またはFOUC。 理論的には、これらすべての間にマークアップを挿入できます。 完成した出力と一致するようにタグを付けますが、これは実際にはほとんど不可能であり、特に Shoelace のようなサードパーティのコンポーネント ライブラリの場合はそうです。

Web コンポーネント登録コードの移動

問題は、Web コンポーネントに必要な処理を実行させるコードが、ハイドレーションが発生するまで実際には実行されないことです。 この投稿では、そのコードをすぐに実行する方法を見ていきます。 実際、すぐに。 Web コンポーネント コードをカスタム バンドルし、手動でスクリプトを直接ドキュメントに追加する方法を見ていきます。 そのため、すぐに実行され、実行されるまでドキュメントの残りの部分がブロックされます。 これは通常、ひどいことです。 サーバー側レンダリングの要点は、 JavaScript が処理されるまで、ページの処理をブロックします。 しかし、一度完了すると、ドキュメントが最初にサーバーから HTML をレンダリングするときに、Web コンポーネントが登録され、適切なコンテンツがすぐに同期的に発行されます。

私たちの場合、私たちは ただ ブロッキング スクリプトで Web コンポーネント登録コードを実行しようとしています。 このコードはそれほど大きくはありません。その後のアクセスに役立つキャッシュ ヘッダーをいくつか追加することで、パフォーマンスへの影響を大幅に軽減することを検討します。 これは完璧な解決策ではありません。 ユーザーが初めてページを閲覧すると、そのスクリプト ファイルが読み込まれている間、常にブロックされます。 後続の訪問は適切にキャッシュされますが、このトレードオフ ではないかもしれない あなたにとって実行可能である - eコマース、誰か? とにかく、プロファイリングし、測定し、アプリの正しい決定を下してください。 さらに、将来、Next.js が DSD と Web コンポーネントを完全にサポートする可能性は十分にあります。

始める

これから見るコードはすべて このGitHubリポジトリ および Vercel でここに展開. Web アプリは、いくつかの Shoelace コンポーネントを、水分補給時に色と内容が変化するテキストと共にレンダリングします。 Shoelace コンポーネントが既に適切にレンダリングされている状態で、テキストが「Hydrated」に変更されたことを確認できるはずです。

カスタム バンドル Web コンポーネント コード

最初のステップは、すべての Web コンポーネント定義をインポートする単一の JavaScript モジュールを作成することです。 私が使用している Shoelace コンポーネントのコードは次のようになります。

import { setDefaultAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry";

import "@shoelace-style/shoelace/dist/components/tab/tab.js";
import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js";
import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js";

import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";

setDefaultAnimation("dialog.show", {
  keyframes: [
    { opacity: 0, transform: "translate3d(0px, -20px, 0px)" },
    { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
  ],
  options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});
setDefaultAnimation("dialog.hide", {
  keyframes: [
    { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
    { opacity: 0, transform: "translate3d(0px, 20px, 0px)" },
  ],
  options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});

の定義をロードします。 および ダイアログのいくつかのデフォルト アニメーションをオーバーライドします。 十分に単純です。 しかし、ここで興味深いのは、このコードをアプリケーションに取り込むことです。 私達 単に import このモジュール。 これを行うと、通常の JavaScript バンドルにバンドルされ、ハイドレーション中に実行されます。 これにより、回避しようとしている FOUC が発生します。

Next.js にはカスタム バンドル用の webpack フックがいくつかありますが、ここでは使用します。 ネジ 代わりは。 まず、それをインストールします npm i vite 次に、 vite.config.js ファイル。 私は次のようになります。

import { defineConfig } from "vite";
import path from "path";

export default defineConfig({
  build: {
    outDir: path.join(__dirname, "./shoelace-dir"),
    lib: {
      name: "shoelace",
      entry: "./src/shoelace-bundle.js",
      formats: ["umd"],
      fileName: () => "shoelace-bundle.js",
    },
    rollupOptions: {
      output: {
        entryFileNames: `[name]-[hash].js`,
      },
    },
  },
});

これにより、Web コンポーネントの定義を含むバンドル ファイルがビルドされます。 shoelace-dir フォルダ。 に移動しましょう public Next.js がそれを提供するようにします。 また、最後にハッシュを付けて、ファイルの正確な名前を追跡する必要もあります。 ファイルを移動し、バンドル ファイルの名前で単純な定数をエクスポートする JavaScript モジュールを作成する Node スクリプトを次に示します (これはすぐに役立ちます)。

const fs = require("fs");
const path = require("path");

const shoelaceOutputPath = path.join(process.cwd(), "shoelace-dir");
const publicShoelacePath = path.join(process.cwd(), "public", "shoelace");

const files = fs.readdirSync(shoelaceOutputPath);

const shoelaceBundleFile = files.find(name => /^shoelace-bundle/.test(name));

fs.rmSync(publicShoelacePath, { force: true, recursive: true });

fs.mkdirSync(publicShoelacePath, { recursive: true });
fs.renameSync(path.join(shoelaceOutputPath, shoelaceBundleFile), path.join(publicShoelacePath, shoelaceBundleFile));
fs.rmSync(shoelaceOutputPath, { force: true, recursive: true });

fs.writeFileSync(path.join(process.cwd(), "util", "shoelace-bundle-info.js"), `export const shoelacePath = "/shoelace/${shoelaceBundleFile}";`);

コンパニオン npm スクリプトは次のとおりです。

"bundle-shoelace": "vite build && node util/process-shoelace-bundle",

それはうまくいくはずです。 私のため、 util/shoelace-bundle-info.js 現在存在し、次のようになります。

export const shoelacePath = "/shoelace/shoelace-bundle-a6f19317.js";

スクリプトのロード

Next.js に入りましょう _document.js ファイルを開き、Web コンポーネント バンドル ファイルの名前を取得します。

import { shoelacePath } from "../util/shoelace-bundle-info";

次に、手動でレンダリングします タグは . これが私の全体です _document.js ファイルは次のようになります。

import { Html, Head, Main, NextScript } from "next/document";
import { shoelacePath } from "../util/shoelace-bundle-info";

export default function Document() {
  return (
    
      
        
      
      
        
); }

そして、それはうまくいくはずです! Shoelace の登録はブロック スクリプトで読み込まれ、ページが最初の HTML を処理するとすぐに利用できるようになります。

パフォーマンスの向上

そのままにしておくこともできますが、Shoelace バンドルにキャッシュを追加しましょう。 次のエントリを Next.js 構成ファイルに追加して、これらの Shoelace バンドルをキャッシュ可能にするように Next.js に指示します。

async headers() {
  return [
    {
      source: "/shoelace/shoelace-bundle-:hash.js",
      headers: [
        {
          key: "Cache-Control",
          value: "public,max-age=31536000,immutable",
        },
      ],
    },
  ];
}

その後、サイトを参照すると、Shoelace バンドルが適切にキャッシュされていることがわかります。

DevTools Sources パネルが開き、読み込まれた Shoelace バンドルが表示されます。
Next (または任意の SSR フレームワーク) で Web コンポーネントを使用する

Shoelace バンドルが変更されると、ファイル名が変更されます ( :hash 上記のソース プロパティの一部)、ブラウザはそのファイルがキャッシュされていないことを検出し、単にネットワークから新しいファイルを要求します。

包み込む

これは多くの手作業のように思えたかもしれません。 そしてそうだった。 残念なことに、Web コンポーネントは、サーバー側のレンダリングに対してすぐに使用できる優れたサポートを提供していません。

しかし、それらが提供する利点を忘れてはなりません。特定のフレームワークに縛られない高品質の UX コンポーネントを使用できることは素晴らしいことです。 次のようなまったく新しいフレームワークを試すことができるのも良いことです。 コールテン、ある種のタブ、モーダル、オートコンプリート、またはその他のコンポーネントを見つける(または一緒にハックする)必要はありません。

タイムスタンプ:

より多くの CSSトリック