NIKKEI Digital Recruiting Site

日経電子版を支える広告技術

本稿では、現在の日経電子版モバイル Web(以下、日経電子版)における広告掲示の技術について解説する。

広告とサイトパフォーマンスの関係

dev.to に代表されるような「高速」と言われる Web サイトは、CDN や ServiceWorker などを駆使して表示に関して適切なチューニングを行っており、日経電子版もほぼ同じ思想の元チューニングを行っているが、一点大きな違いを挙げるとするならば、広告表示の有無が挙げられる。

現在の日経電子版では 7 タイプの広告を表示しており、うち 4 タイプは外部 DSP からの配信を行っている。

その他にも様々なサイトトラッキングのサードパーティスクリプトを導入しているが、ご存知の通りこれらはメインとして打ち出したいコンテンツとはほぼ関連性はなく、パフォーマンス観点から見れば劣化させる要因でしかない。広告が無ければもっと速く表示できるのに…と思ったことはないだろうか。それは正しい感覚である。

しかしながら、Web サイトやサービスの特性にも依るが、これら広告は Web サイトを運営し、ビジネスを行っていく上で必要なものであることもまた正しい。「サイトが遅くなるから広告は入れません」という要望は通常通らないだろう。

広告は遅いから仕方ない、と諦めない

だからと言ってそのまま渡されたタグを設置して広告を表示する、と諦めないでほしい。なぜ広告は遅いのか、遅くする原因は何なのか、少しでも速く軽くするためにはどうすればよいかを計測し、検討する価値は十分にある。さらには、フロントエンドの高速化の為に行う施策に合わせて広告の表示手法を考える必要が出てくる。

本章では実際に日経電子版での広告表示に関して計測したこと、そして検討・実施したことについて解説を行う。

なお注意点として、外部から提供される SDK スクリプトを許可なく改変することで保証の対象外や著作権の違反になったりするケースもある。必ず提供元と協議の上、問題がないことの許諾を得た上で実行してほしい。

また、完全に隠蔽されてロジックを挟む余地のない手法で提供される広告も残念ながら存在する。その場合はお手上げであるが、幸い日経電子版では SDK 経由で広告配信サーバと通信する手法であったため、導入側での対策を講じることが可能であった。

サイトパフォーマンスを劣化させる原因とその対策

広告の掲出に際して、具体的にパフォーマンスを劣化させる原因は何なのだろうか。先に結論を述べると、「広告表示に関わるリクエスト、スクリプティング、レンダリング全て」である。日経電子版では、これらの要素を「SDK の初期化」、「広告の取得」、そして「広告のレンダリング」の 3 つのフェーズに分けてチューニングを行った。

SDK の読み込みとさらなる追加リクエスト

通常の広告は「導入が簡単」なことにより指定された JavaScript のタグの埋め込みと、少しの実装コードで表示されるだろう。この時、大抵の広告 SDK は内部で追加のリクエストを発行している。具体的には、

# 指定された埋め込みタグ。これは大抵Bootstrapファイル
<script src="https://example.com/advert.js"></script>
↓
# 実際にロジックが書かれたファイルがロードされる
<script src="https://example.com/versions/advert-ktkj30vkqw2.js"></script>
↓
# さらに必要なスクリプトをロードするものもある
<script src="https://example.com/versions/advert-ktkj30vkqw2-e.js"></script>
↓
以下必要に応じて追加されていく

というロード形式がほとんどである。不変なエンドポイントから内部のバージョンファイルをロードすることで、SDK の改修に対してもユーザーは読み込む URL を変更する必要がないため、これは正しい実装であると言える。

しかしながら、SDK の初期化だけで最低+2 回の HTTP リクエストがされることになる。さらにスクリプティングも行われるため、その間のレンダリングはブロックされる。

まず、一番最初の <script> タグを <head> でロードするのはそれだけで悪手であり、 async を付けて遅延ロードすることが推奨される。

最終的には、順序通りに 3 番目のスクリプトまでロードされてようやく広告 SDK の初期化が完了となる。このフローを最適化することで広告を掲出するまでの時間を改善する。

対策

昨今のフロントエンドは「ビルドシステム」を持ち、 rollupwebpack といったものでスクリプトをバンドルして配信するのが今や一般的だろう。そこで、日経電子版ではこれらの追加ロードされる SDK を事前に解決し、バンドルした上で CDN から配信する手法を採用している。

具体的には、1 番目のスクリプトを取得し、追加ロードされる箇所を特定し、そこに書かれている URL をさらに fetch、またそのレスポンスからさらなる追加ロードを検出、fetch…を繰り返し、最後にそれらを結合、minify して CDN から配信する。

これにより SDK 本体は 1 本の HTTP リクエストで完了し、確実に minify され、さらにキャッシュも十分に効く。またビルドハッシュを付与することで、SDK 側に改修があった場合も再ビルドすることで改修後のものを使用できるようにしている。また、配信されるコードは一部 minify するとエラーになるコードがあったため、結合時にパッチを当てて回避するなどの最適化も行っている。これにより SDK の初期化までの実行コストは O(N)から O(1)に改善し、実測値で初期化まで 100ms 程改善、スクリプトのバンドルサイズも 30%程度改善した。

スクリプティングと同期的メインスレッド至上主義

続いて広告の取得からスクリプティングまでの最適化だが、大抵の広告において、そのコードはメインスレッドを積極的に侵してくる。もちろん広告は「確実に表示される」ことが至上命題であるが、メインスレッドで動作することが前提として書かれたスクリプトがあまりに多く、昨今の高速化を KPI とするサイトとは些か相性が悪いのが現状である。実際に遭遇した実装として代表的なものを下記に挙げる:

  • document.write() で同期的に <script> タグを書き出す
  • Viewable 判定の為にリフローを起こし得るプロパティに頻繁にアクセスする
  • 内部で setInterval() を実行し、タイマーがメインスレッドに常駐する
  • 動画広告など、大量の追加ライブラリの読み込みを強制する

これらは IE6 といったレガシーブラウザの時代から広告表示をさせるべく受け継がれてきた実装が原因だろう。動画広告に至っては video.js などの重いスクリプトに依存しており、導入側の負担は増えるばかりである。日経電子版では First View に必要の無いコンテンツは遅延ロードする仕組みだが、上記のような同期的なロジックと非常に相性が悪い。

さらに、常に DOM アクセスを要求するためクライアントサイドのどこかで処理する必要があり、Worker やサーバーサイドで前処理を行うことも難しい。

対策

DOM があり、かつ別スレッドで動作可能という条件であれば、iframe での別スレッド化が思いつく。そこで SDK から広告データ取得と解析までを別の iframe 内で実行する案について検討した。

SDK とサードパーティスクリプトの読み込み

メインスレッドで遅延ロード vs iframe 内でのロードのベンチマークを取った結果、若干の揺れはあったものの iframe 内でロードを行う方が Start Render Time,及び Visually Complete Time の改善が見られ、サブスレッドで初期化するほうが効率が良いという結果が得られた。

左がメインスレッド、右が iframe による初期化

また広告関連の SDK をグローバル空間に追加することを回避でき、iframe 内で SDK 初期化、サードパーティスクリプトの読み込みを行う方針にも適合した。iframe 一回分の HTTP リクエストは増えてしまうが、これも事前にビルド、CSS、JS を minify して埋め込んだ HTML ファイルを CDN から配信することで影響を小さく抑えている。

サブスレッドでのレンダリング制御

最後にレンダリングの制御について検討した。iframe 内で広告周りのロジックを実行する限りはメインスレッドに影響を与えないため、可能な限り iframe 内で処理すべく前述の同期的な問題も同時に対応した。具体的には下記のような手法で解決している。

document.write()

document.write() は強制的に HTML を書き出すメソッドであるが、JavaScript ではこのメソッドを一時的にオーバーライドすることでメソッドの挙動を上書きすることが可能である。その性質を利用し、擬似的にバッファリングを行う挙動に置き換えて対応した。実行後は元の挙動に戻してやる必要がある。

const nativeWrite = document.write;
// Buffering
let buffer;
// 広告内部で実行されるメソッドの挙動をを一時的に置き換える
document.write = buf => {
    buffer = buf;
};
// 広告レンダリング
...
// 元に戻す
document.write = nativeWrite;

// 書き出される予定のバッファがが得られる
console.log(buffer);

これにより任意のタイミングで広告表示が行える。iframe で実行していることも幸いし、document.write() 差し替えの影響はゼロであった。

リフロー制御と setInterval()

タイマースレッドを iframe 内に移したことでメインスレッドを汚さなくなり、結果クリーンに保つことができた。また広告用の iframe はメインウインドウには見えないように配置しているため、タイマー内での DOM アクセスについてもリフローは起きなかった。

動画広告

全ての追加ロードされるスクリプトを精査し、我々では対応できないもののみ仕方なくメインスレッドでの実行を許可することにした。

これらの対策を施すことによって、メインスレッドと広告ロジックを分離させることに成功した。

最適化の導入とロジック・ビューの分離

ここまでは個別の最適化について述べたが、これらを実際に Web サイト上に実装した手法について解説する。

Shadow DOM

通常なら広告自身の表示も iframe 内で行うが、日経電子版ではさらに一歩進めて Shadow DOM での表示を試みている。きっかけとなったのは下記のエントリである:

https://css-tricks.com/playing-shadow-dom/

Shadow DOM で表示するメリットは下記のとおりである:

  • メインウインドウと同じレンダリングロジックが使える
  • スタイル・スクリプトはカプセル化されて外部に影響を与えない

特に後者が大きく、コンテンツと広告の分離には適している。実際に Shadow DOM vs iframe でのベンチマークを取ると、概ね Shadow DOM の方がパフォーマンスは良好であり、特に Chrome では顕著だった。また、iframe の場合は広告サイズに合わせて高さをフィットさせる必要があったのが、Shadow DOM であれば広告のサイズによる高さ制御はメインウインドウのレンダリングロジックに従うため、その計算処理も省くことが可能である。リフローは同様に発生するがブラウザに任せるほうが良いだろう。

しかしながら、前述の動画広告などはメインスレッドへのスクリプトロード、及び実行が強制されていたため、完全に Shadow DOM 内のカプセル化は不可能であった。これは広告側の対応が必要になるため iframe での表示を強制することで対策した。

現在の日経電子版ではテキスト、画像レクタングル、インフィードといった静的な広告については Shadow DOM で表示している。スタイルのカプセル化手法については後述する。

Viewable

広告の表示は、「広告を表示する要素がビューポートに入ったら表示する」といういわゆる Viewable と呼ばれる掲出が一般的になりつつある。これを検出するには、従来は setInterval() などで要素の inview を評価する必要があり、タイマースレッドの常駐はこれをレガシーブラウザで行うための実装だろう。しかしながら、最近ではそれに適した API がブラウザから使用できる。

Intersection Observer

IntersectionObserver はそのままズバリ要素の Intersection を検知できるビルトインクラスであり、特に広告関連にはうってつけの API であると言える。未だに Working Draft であるが、Chrome, Firefox, Edge などの主要ブラウザでは既に使用でき、また Polyfill もあるためにすでに利用できる段階であると言える。実際の導入においては仕様に記載されていない挙動の問題などもあったが、要素検出という点においては利用可能であると判断した。

W3C: https://www.w3.org/TR/intersection-observer/ Can I use https://caniuse.com/#search=IntersectionObserver

const observer = new IntersectionObserver(
    entries => {
        entries.forEach(entry => {
            if (entry.isIntersecting === false) {
                return;
            }
            // 要素がInViewになった
            // do something...

            // 監視の解除
            observer.unobserve(el);
        });
    },
    {
        // 検出量
        threshold: [0]
    }
);
// 監視開始
observer.observe(el);

広告に関しては Y 方向のみの検出で良かったため、日経電子版では Safari など非サポートブラウザに向けて独自に小さな Polyfill を書いて対応している(実際は rootMargin などもう少し仕様に即した実装を行っている):

const polyfillIntersectionObserver = (() => {
    let observerList = [];
    let isProcessing = false;

    const _loopProcess = () => {
        observerList = observerList.reduce((prev, next) => {
            try {
                const { el, callback } = next;
                const rect = el.getBoundingClientRect();
                if (rect.top - window.innerHeight < 0) {
                    // 要素がInViewになった
                    // do something...
                    return prev;
                }
            } catch (e) {
                console.error(e);
                return prev;
            }
            prev.push(next);
            return prev;
        }, [])
        if (observerList.length === 0) {
            isProcessing = false;
        } else {
            window.requestAnimationFrame(_loopProcess);
        }
    };

    // Expose function
    return (el, callback) => {
        observerList.push({
            el,
            callback
        })
        if (!isProcessing) {
            isProcessing = true;
            _loopProcess();
        }
    };
})();

Mutation Observer

IntersectionObserver は既知の要素について検出できるが、遅延ロードなどによる途中からの要素の DOM ツリーへの追加は検出できない。そのため、MutationObserver を併用してこの問題を解決している。MutationObserver も IE11 からとモダンなブラウザでは対応済みであるため、古いブラウザ対応がなければ利用可能だろう。

Can I use https://caniuse.com/#search=MutationObserver

// Observe added targets dynamically
new MutationObserver(mutations => {
    mutations.forEach(mutation => {
        // Find added nodes which matches to expected selector
        [].slice
           .call(mutation.addedNodes)
           .filter(node => node.nodeType === Node.ELEMENT_NODE)
           .forEach(node => {
               const targets = [].slice.call(
                   node.querySelectorAll(selector)
               );
               if (node.matches(selector)) {
                   targets.push(node);
               }
               targets.forEach(el => {
                   // 要素の追加検出
                   // do something...
               });
           })
        ;
    });
}).observe(document.body, {
    childList: true,
    subtree: true
});

Responsive

広告はデスクトップターゲットとモバイルターゲットで掲出する広告を分ける必要性もある。日経電子版はレスポンシブデザインを採用しており、一枚の HTML ドキュメントに対して適切に広告を振り分けるために、ユーザーエージェントの判定はもちろん、レイアウト変更の検出で広告を動的にスイッチする機構も実装している。

window.matchMedia

window.matchMedia でレイアウトの変更を検知し、モバイル・PC 毎に広告をスイッチする。

let displayType = 'desktop';
const mql = win.matchMedia('(min-width: [threshold]px)');
mql.addListener(m => {
    const target = m.matches ? 'desktop' : 'mobile';
    if (target === displayType) {
        return;
    }
    displayType = target;
    views.forEach(v => {
        // 広告スイッチ処理
        this.switchView(v, target);
    });
});

上記のように、これまで広告を適切なタイミングで表示するにはタイマー処理などが必要であったが、昨今のクライアントサイドではそれらに適した API が利用可能になり始めている。またこれらは組み込みであるため、自前で行うよりもパフォーマンスの向上が期待できるだろう。

日経電子版でのアーキテクチャ

最後に前述の最適化処理をまとめた日経電子版でのアーキテクチャについて述べる。日経電子版の広告では iframe-based VC と呼んでいるアーキテクチャで広告の管理と表示を行っている。

[C] コントローラ iframe

メインウインドウ上に見えない状態で非同期に配置され、SDK の初期化と広告リクエスト、レイアウト変更の検知、同一広告の排他処理などビュー以外の処理全てを担当する。各ビューはこのコントローラと postMessage のイベントインターフェースで通信し、必要な情報を受け取る。この iframe ももちろん事前ビルドにより圧縮、SDK の埋め込みを行い、CDN から HTTP2 で配信される。

取得した広告データ HTML を静的に解析し、コンテンツとスクリプトに分離した上でビューに渡すことで、ビューはコンテンツを先行してレンダリングし、追加で必要なスクリプトは別途遅延ロードさせる。またページ上に同一の広告を表示させないための排他処理も我々で実装し、表示に関する制御を可能な限りこちらでコントロールできるようにしている。

[V] ビュー iframe/Shadow DOM

メインウインドウ上で IntersectionObserver、または MustationObserver による検知後にインスタンス化され、コントローラへ接続通知をpostMessageで送信する。コントローラはこのメッセージを受信、広告を取得し、レンダリングに必要な情報のみをビューに戻すので、それを元にビューはレンダリングのみを行うだけでよく、メインスレッドでの余分なスクリプティングは最小限に抑えられる。

View はインスタンス化される際に Shadow DOM で表示すべきかどうかを判定し、自動で iframe へフォールバックする設計のため、どの広告をどのように出すかは考えなくてよい。

なお、Shadow DOM の場合はコントローラから追加でカプセル化用の CSS を別途イベントで受取り、あらかじめ CSS の解析を済ませておく。iframe の場合はそれ用に最適化した事前ビルドファイルをロードするのみである。iframe/Shadow DOM どちらの場合も同じpostMessageのイベントインターフェースを実装しておくことで、コントローラ側での不要な分岐処理をしなくて良いようにしている。ページ上の広告表示フローは下図のようになる:

通常通り表示する場合に比べてフローが複雑化しているのは否めないが、それを補う高速化の恩恵を得られ、かつ遅延ロードされる広告要素にも対応できた。CDN でのキャッシュ、iframe を利用した別スレッドでの広告ロジックの最適化、メインウインドウ上での View のレンダリング処理の最小化が概ね良好な結果を得る原因になったと評価している。

Web における広告技術の展望と希望

広告配信側技術のアップデート

配信される DSP のコード全てに目を通した感想だが、やはり DOM に依存しきっているため、iframe での別スレッド化くらいしか対策が打てないのが悔しい点である。日経電子版では以下のようなさらなる取組みと検証を進めている:

  • コントローラの WebWorker 化。DOM の癒着を剥がし切れない DSP のコードがある限り現状は難しい。
  • ビューの Custom Elements 化。Shadow DOM の次のロードマップとして、広告要素もページ上で同格の DOM 要素として扱えることが期待される。

今回は導入側で出来る限りの最適化を行ったが、これらは広告を配信する側で対応できることがほとんどである。歴史的経緯と安定稼働が重要なのは承知の上ではある。しかしレガシーブラウザを切り離す、モダンな API を使う、モバイルでもパフォーマンスが出せる実装など、広告を提供する側の Web フロントエンド技術はそろそろアップデートする時期に来ているのではないか。

しかしながら、これらの対応が難しい現状では、フロントエンドの領域において広告はまだまだ最適化の余地があり、取り組む価値のあるものではないだろうか。

本章では「広告の最適化」という、おおよそフロントエンドの技術領域について触れられることがないであろうテーマについて解説した。何か一つでも最適化するヒントになれば幸いである。

関連リンク: dev.to: https://dev.to/

杉本吉章
ENGINEER杉本吉章

Entry

各種エントリーはこちらから

キャリア採用
Entry
新卒採用
Entry