症状
Drupal 11.3 のサイトを Safari 26 で開くと、あるページでボタンが1つも動かなくなりました。クリックしても無反応、チェックボックスを入れても送信ボタンが disabled のまま。同じページを Chrome で開くと正常です。
「Safari だけ動かない」は普通、特定のスクリプト1つの互換性問題です。ところがこれは違いました。そのページの Drupal.behaviors が1つも登録されていない。特定の機能ではなく、Drupal の JS 挙動が丸ごと死んでいます。
まず再現環境を疑いから外す
「Chrome は正常、Safari だけ不可」のとき、最初にやるのは実機 Safari の拡張機能やキャッシュを疑うことではありません。クリーンな WebKit で再現するかを見ます。拡張の注入やプロファイル汚染を排除しないと、サイトのバグと環境のバグを取り違えます。
playwright-cli open --browser=webkit
クリーンな WebKit で同じ症状が出ました。これでサイト側のバグと確定します。ここを飛ばして実機 Safari だけで調べると、原因の切り分けが1段増えます。
コンソールに1つだけ uncaught TypeError
WebKit のコンソールを見ると、そのページの読み込み中に uncaught な TypeError が1つ出ていました。Drupal.htmx か Drupal.debounce が undefined、という類のものです。
ここが核心でした。この1つの例外が、Drupal.attachBehaviors() の実行を途中で止めています。 attachBehaviors は登録された behavior を順に呼ぶループなので、途中で throw されるとそれ以降が回らない。結果、そのページの behavior が「一切登録されていない」ように見えます。1つの例外が、無関係な全機能を巻き添えにしていました。
なぜ Drupal 11.3 で起きるのか
Drupal 11.3 で BigPipe が htmx ベースに変わりました。BigPipe は、ページの外枠を先に返して、重いプレースホルダを後からストリームで差し込む仕組みです。11.3 ではこのストリーム処理が big_pipe.commands.js になり、本文の置換チャンクを MutationObserver で拾って Drupal.htmx.mergeSettings() や addAssets() を呼びます。
問題はロード順です。
Drupal.htmx/Drupal.debounceを定義する core のライブラリは、ページフッターで読み込まれます- 一方 BigPipe の本文ストリーム処理は、そのフッターより先に走ることがあります
Chrome ではタイミングがずれて顕在化しませんが、Safari 26 ではストリーム処理がフッターの core ロードを追い越します。すると Drupal.htmx がまだ undefined のまま Drupal.htmx.mergeSettings() が呼ばれ、uncaught TypeError。そこから前述の attachBehaviors 中断に連鎖します。
core/misc/form.js が Drupal.debounce を IIFE の引数に取っているのも同じ構図で、フッターの debounce ロードより先に評価されると undefined を掴みます。
対策: 隙間を埋めるガードを head に置く
根本原因は「本物の core が来る前に呼ばれる」ことなので、本物が来るまでの隙間を最小実装で埋めます。依存ゼロのブートストラップを <head> に置き、Drupal.htmx と Drupal.debounce を先に定義しておきます。
(function () {
'use strict';
var Drupal = (window.Drupal = window.Drupal || {});
// core/misc/htmx/htmx-utils.js の mergeSettings と同等の再帰ディープマージ。
function mergeSettings(current) { /* ... 省略 ... */ }
if (
!Drupal.htmx ||
typeof Drupal.htmx.mergeSettings !== 'function' ||
typeof Drupal.htmx.addAssets !== 'function'
) {
var existingHtmx = Drupal.htmx || {};
Drupal.htmx = {
mergeSettings: existingHtmx.mergeSettings || mergeSettings,
// 隙間でのみ呼ばれる暫定実装。即時 resolve で add_css コマンドの throw を防ぐ。
addAssets: existingHtmx.addAssets || function () { return Promise.resolve(); },
};
}
if (!Drupal.debounce) {
// core/misc/debounce.js と同等の最小実装。
Drupal.debounce = function (func, wait, immediate) { /* ... 省略 ... */ };
}
})();
設計で効いているのは4点です。
依存ゼロで <head> に置く。 core より前に走るので、core を IIFE 引数に取ると undefined になります。window.Drupal を自前で用意します。ここを間違えると、対策のスクリプト自体が ReferenceError で死にます(実際に一度踏みました)。
実行順序が命。 このガードは drupal.js より後、big_pipe より前に走る必要があります。drupal.js は window.Drupal を無条件に初期化するので、その前に置くと消されます。big_pipe はフッターなので、head に置けば確実に先行します。この順序を壊すと対策になりません。
冪等であること。 core はフッターで Drupal.htmx を無条件代入で上書きします。つまり定常状態では本物が使われ、ガードは「ストリーム初期の隙間」でだけ働きます。上書きされる前提で、最小実装を置いています。
addAssets は即時 resolve する。 隙間の段階では実アセットを読めませんが、Promise を返して即 resolve すれば add_css コマンドの throw を防げます。プレースホルダの CSS は通常メインの集約に含まれるので、見た目の影響は出ません。
対策しても、critical な操作は behavior に依存させない
ガードで attachBehaviors の中断は塞げますが、それでも取引フローや同意ゲートのような critical な操作は、behavior に頼らない方針にしました。
理由は、attachBehaviors が中断する経路は BigPipe 以外にもあり得るからです。万一また止まっても死なないように、重要な操作は Drupal.behaviors / once / jQuery に依存せず、素の document へのイベント委譲で実装します。
// behavior が全滅しても動く。document への委譲+依存ゼロ。
document.addEventListener('click', function (e) {
var btn = e.target.closest('[data-critical-action]');
if (!btn) { return; }
// ... 処理 ...
});
「フレームワークの初期化に乗る」のが普通ですが、初期化そのものが落ちうる経路では、乗らない実装を併用します。落ちても死なない層を、金銭が動く操作にだけ敷いています。
検証の順序
このクラスのバグは、机上で JS を連投すると泥沼にはまります。順序を固定しました。
- クリーンな WebKit で再現する(
playwright-cli open --browser=webkit)。環境要因を先に排除 - コンソールの uncaught 例外を1つ特定する。behavior 全滅の起点はほぼこれ
- ロード順(どのスクリプトが本物より先に評価されているか)を確認する
- 最終確認だけ実機 Safari で行う
「Chrome は動くのに Safari が動かない」を、いきなり実機 Safari の勘で直そうとしないことです。
まとめ
- Drupal 11.3 の BigPipe は htmx 化され、本文ストリーム処理が
Drupal.htmx/Drupal.debounceを呼ぶ。 これらはフッターロードのため、Safari 26 ではストリームが追い越して undefined を掴む - 1つの uncaught TypeError が
attachBehaviorsを中断させ、そのページの behavior が全滅する。 症状は「特定機能の不調」でなく「全ボタンが死ぬ」 - 対策は、依存ゼロのガードを
<head>(drupal.jsの後・big_pipeの前)に置き、隙間を最小実装で埋める。 core が後から本物で上書きするので冪等 - critical な操作は behavior に依存させず、
documentへのイベント委譲で実装する。 初期化が落ちても死なない層を用意する - 検証はクリーン WebKit での再現から始める。 実機 Safari の勘で JS を連投しない
この記事は、事業・Webサイトの売買プラットフォーム RIKKA M&A を開発する中で得た知見です。RIKKA M&A は GitHub リポジトリを解析する技術デューデリジェンスを備え、Drupal 11 / PHP / LLM で構築・運用しています。

