壁紙アプリのAdMob収益が、半年ほど横ばいで止まっていました。アーティスト・クリエイターの廣川政樹(@dolice)です。2014年から個人でアプリを作ってきて、累計5,000万ダウンロードを超えたあたりから、広告フォーマットを「個別に」最適化することの限界を感じるようになりました。Rewardedを伸ばすと Interstitial の表示機会が削られ、App Open を入れると Rewarded の視聴完了率が落ちる。3つの広告フォーマットがお互いを食い合っていたのです。
2026年の春から9週間かけて、AdMobの3フォーマットを「3層連携」という発想で組み直しました。役割分担を明確にし、排他制御を入れ、ARPDAU(1日アクティブユーザーあたりの広告売上)を1.4倍まで押し上げています。AdMobの月次収益がピーク時で100万円超えにまで戻った今、その設計と運用ログをまとめておきます。
なぜ「3層連携」で考える必要があるのか
AdMobの公式ドキュメントは、Rewarded、Interstitial、App Openそれぞれの設計指針を別個に解説しています。これらは単独で読むと、どれも「ユーザー体験を損なわずに表示頻度を上げよ」と書いてあります。当然です。問題は、3つを同じアプリに乗せたときに、ガイドラインが互いに干渉することにあります。
私が最初に経験した衝突は、こうでした。Rewardedで「広告を見ると壁紙が3枚追加で見られる」というインセンティブを設計したのですが、その直前にApp Open広告が出てしまい、ユーザーが「広告ばかり出てくる」と離脱する。Crashlyticsでクラッシュは出ていないのに、Rewardedのインプレッション数が3割落ちました。原因は単純で、ユーザーの集中力という有限リソースを、3つの広告フォーマットが同時に奪い合っていたのです。
3層連携の本質は、3つの広告フォーマットに「主役・調味料・漏れバケツ」という明確な役割を割り振り、お互いに発火タイミングを譲り合う設計を入れることです。これを言語化したのは、累計5,000万DLの壁紙アプリ群を運用していて、収益の天井が単一フォーマットの最適化では超えられないと気づいた瞬間でした。
3つの役割分担 — 主役・調味料・漏れバケツ
私が9週間で固めた役割分担は、次の通りです。
主役はRewarded広告です。ユーザーが「自分の意思で広告を見て、何かを得る」というポジティブな交換が成立する唯一のフォーマットだからです。eCPMも他より2〜3倍高く、視聴完了したユーザーは継続率も上がります。設計の出発点は、この主役の表示機会を最大化することに置きます。
調味料はInterstitialです。これは「セッションの区切り目」に挿入することで、ユーザーの操作の流れに乗せます。壁紙アプリでいえば「壁紙を10枚スワイプして見たあとのカテゴリ切り替え時」が良いタイミングです。逆に、ユーザーが「能動的に何かを選ぼうとしている瞬間」に出すと、CTRは上がっても離脱が増えます。私はこれを2026年の初頭に1度試して、Day 7のリテンションが11ポイント落ちたことを覚えています。
漏れバケツはApp Openです。これは「他の2つが発火しなかった場合の保険」として位置づけます。Rewardedが視聴されず、Interstitialも頻度キャップで抑制されたセッションで、最後の収益化機会としてApp Openを差し込みます。ここを「主役級」に扱うと、起動のたびに広告が出てユーザー体験が破綻します。
Rewarded広告を主役に据えた3つの工夫
Rewardedを主役にすると決めたら、ユーザーが「広告を見たい」と思う瞬間を増やすことに資源を投下します。私が壁紙アプリで効いた工夫は3つです。
1つ目は「コンテンツの可視性を交換材料にする」設計です。無料ユーザーには壁紙の高解像度版が見えない状態で表示し、Rewarded広告を視聴すると48時間だけアンロックされる仕組みにしました。これは「機能制限」ではなく「視覚的なヒント」を見せておくのがコツです。低解像度のままだと「見たい」という欲求が生まれません。
2つ目は「Rewardedを成立させた回数に応じてUIが進化する」見せ方です。視聴3回でカテゴリのソート機能が解放、10回でテーマカラーの選択肢が増える、というように、視聴履歴をローカルに保存して報酬を段階化します。Firebase Remote Configで段階の閾値を変えられるようにしておくと、A/Bテストもしやすくなります。
3つ目は「Rewardedの視聴可能枠を有限にする」設計です。1日10回まで、と上限を設けると、ユーザーは「今日のRewardedを使い切ろう」という心理に傾きます。これは飲食店の「本日限定」と同じ仕組みで、視聴完了率が私のアプリでは18%上がりました。
// Rewardedの視聴枠管理(React Native + AsyncStorage)
const REWARDED_DAILY_CAP = 10;
async function canShowRewarded(): Promise<{ allowed: boolean; remaining: number }> {
const today = new Date().toISOString().slice(0, 10);
const raw = await AsyncStorage.getItem(`rewarded_count_${today}`);
const count = raw ? parseInt(raw, 10) : 0;
return {
allowed: count < REWARDED_DAILY_CAP,
remaining: Math.max(0, REWARDED_DAILY_CAP - count),
};
}
async function recordRewardedView(): Promise<void> {
const today = new Date().toISOString().slice(0, 10);
const key = `rewarded_count_${today}`;
const raw = await AsyncStorage.getItem(key);
const count = raw ? parseInt(raw, 10) : 0;
await AsyncStorage.setItem(key, String(count + 1));
}
このコードは見た目以上に重要で、ユーザーへの「残り枠表示」のためにも使います。「今日あと7回視聴できます」というUIを出すだけで、視聴開始率が変わります。
Interstitialの頻度キャップを「行動粒度」で設計する
AdMobのコンソールには、Interstitialの頻度キャップ機能があります。「1セッションあたり最大2回」「直前の表示から最低60秒」といった設定です。これだけだと不十分でした。
私が「セッション粒度」のキャップから「行動粒度」のキャップに切り替えたのは、こんな問題があったからです。ユーザーが壁紙を眺めているだけのセッションでも、設定画面を開いた瞬間にInterstitialが発火していました。ユーザー視点では「設定を開きたかっただけ」なのに、広告が割り込んでくる。これでは離脱されて当然です。
行動粒度のキャップとは、「ユーザーがアプリ内で何回意味のあるアクションを取ったか」をカウントし、その閾値を超えたらInterstitialを許可する設計です。意味のあるアクションとは、壁紙のスワイプ、カテゴリ選択、お気に入り追加など、ユーザーが能動的に行った操作のことです。
// 行動粒度のInterstitialキャップ
let meaningfulActionsThisSession = 0;
let lastInterstitialAt: number | null = null;
const ACTION_THRESHOLD = 8; // 8アクションで1回許可
const MIN_INTERVAL_MS = 90_000; // 直前から90秒以上経過
function recordAction(actionType: 'swipe' | 'category' | 'favorite' | 'share'): void {
meaningfulActionsThisSession += 1;
}
function shouldShowInterstitial(): boolean {
if (meaningfulActionsThisSession < ACTION_THRESHOLD) return false;
const now = Date.now();
if (lastInterstitialAt && now - lastInterstitialAt < MIN_INTERVAL_MS) return false;
return true;
}
function notifyInterstitialShown(): void {
meaningfulActionsThisSession = 0;
lastInterstitialAt = Date.now();
}
この変更で、Interstitialの表示回数自体は1セッションあたり3回から1.8回に減りましたが、CTRは0.42%から0.71%に上がり、結果としてInterstitialのトータル収益は変わらず、ユーザー離脱率が顕著に下がりました。後で気づいたのですが、これはGoogleのHelpful Content的な発想と似ています。「量より、文脈の質」を優先したほうが、結果として収益も持続する。
App Open広告は「漏れバケツ」として扱う
App Openを「主役級」に置くと、ユーザーが起動するたびに広告が出ます。これはCrashlyticsには表れない「離脱クラッシュ」を生みます。私はApp Openを、他の2つが発火しなかったセッションへの「最後の保険」として位置づけ直しました。
具体的なルールは3つです。1つ目は「コールドスタート時に必ず出す」ではなく「セッション開始から30秒以内に Rewarded か Interstitial が発火していたらスキップする」というロジックを入れること。2つ目は「直前のセッション終了から5分未満の復帰時には絶対に出さない」こと。これはユーザーが「ちょっと別のアプリを開いて戻ってきた」場面で広告を出さないための配慮です。3つ目は「1日3回までの上限を設ける」こと。これも行動粒度のキャップと同じ思想で、ユーザーの体験を守る役割があります。
// App Openの発火可否判定(漏れバケツルール)
const APP_OPEN_DAILY_CAP = 3;
const RETURN_GRACE_MS = 5 * 60 * 1000; // 5分
async function shouldShowAppOpen(
sessionStartMs: number,
rewardedShownAt: number | null,
interstitialShownAt: number | null,
lastSessionEndMs: number | null,
): Promise<boolean> {
// ルール1: 他の2つが30秒以内に発火していたらスキップ
const now = Date.now();
if (rewardedShownAt && rewardedShownAt - sessionStartMs < 30_000) return false;
if (interstitialShownAt && interstitialShownAt - sessionStartMs < 30_000) return false;
// ルール2: 5分以内の復帰時はスキップ
if (lastSessionEndMs && now - lastSessionEndMs < RETURN_GRACE_MS) return false;
// ルール3: 1日3回まで
const today = new Date().toISOString().slice(0, 10);
const raw = await AsyncStorage.getItem(`app_open_count_${today}`);
const count = raw ? parseInt(raw, 10) : 0;
if (count >= APP_OPEN_DAILY_CAP) return false;
return true;
}
この設計に変えてから、App Open広告のCTRは0.18%から0.34%に倍増しました。ユーザーが「邪魔だ」と感じる前のタイミングだけで出るようになったからだと思っています。
3層が同時発火しないための排他制御
3つのフォーマットの役割分担とキャップを決めても、コード上で「同じイベントで複数のフォーマットが発火する」可能性は残ります。たとえば「壁紙をお気に入りに追加した」というイベントで、Rewardedの視聴枠が空いていてかつInterstitialの行動閾値も超えていたら、両方発火してしまいます。
私が入れたのは「AdGate」と呼ぶ排他制御層です。広告フォーマットの表示を直接呼ばず、すべてこのGateを経由させます。Gateは「今このイベントで最適な広告フォーマットを1つだけ選ぶ」役割を持ちます。
// 3層排他制御のAdGate
type AdEvent = 'category_change' | 'favorite_add' | 'app_resume' | 'session_milestone';
async function decideAdToShow(event: AdEvent): Promise<'rewarded' | 'interstitial' | 'appOpen' | 'none'> {
// 優先度1: Rewardedが事前にユーザーアクション(広告視聴ボタン押下等)で要求されている場合
if (event === 'session_milestone' && await canShowRewardedOnMilestone()) {
return 'rewarded';
}
// 優先度2: Interstitialの行動閾値を超えているか
if (shouldShowInterstitial()) {
return 'interstitial';
}
// 優先度3: App Openの漏れバケツルール
if (event === 'app_resume' && await shouldShowAppOpen(/* ... */)) {
return 'appOpen';
}
return 'none';
}
このGateを入れた瞬間、「気づいたら2つ広告が連続で表示されていた」というユーザーレポートがゼロになりました。レビュー欄での「広告が多すぎる」というコメントも、9週間で月15件から3件に減りました。
ARPDAUを測るときに見ている4つの指標
ARPDAUを上げる作業は、単純にインプレッション数を増やすことではありません。私は次の4つの指標を週次でダッシュボードに出して本番運用しています。
1つ目はフォーマット別ARPDAUの内訳です。Rewarded、Interstitial、App Openそれぞれが、1日アクティブユーザーあたりいくら稼いでいるか。合計ARPDAUを見るだけでは、どのフォーマットが伸びているか分かりません。
2つ目はリテンション×ARPDAUのマトリクスです。Day 1、Day 7、Day 30の継続率と、それぞれのコホートでのARPDAUを並べて見ます。Day 30まで残っているユーザーのARPDAUがDay 1の2倍以上あるなら、長期ユーザーの収益貢献を伸ばす設計が機能している証です。
3つ目はフォーマット別のCTRと完了率です。Rewardedの視聴完了率、Interstitialのスキップ率(=途中で閉じられた率)、App Openの即時クローズ率。これらが悪化したフォーマットは、頻度を下げる候補です。
4つ目は「広告未表示セッションの割合」です。3層キャップを入れると、広告がまったく表示されないセッションが必ず存在します。この割合が30%を超えると収益機会を逃しすぎ、5%未満だと表示しすぎです。私は15〜20%を目安にしています。
Firebase BigQuery Export と AdMobのレポートAPIを組み合わせて、これらの指標を毎朝Slackに通知させています。Claude in Chrome に「前日比で異常値が出ていたら原因仮説を3つ出してください」と依頼するワークフローも追加しました。これは別記事で書きましたが、運用負荷を実用範囲に収めるためにかなり効いています。
9週間のタイムライン — 1.4倍までの実測値
参考までに、壁紙アプリ1本(DAU約1.2万)の数値推移を残しておきます。
第0週はベースラインです。ARPDAUは0.038ドル。フォーマット別の内訳はRewarded 0.012、Interstitial 0.018、App Open 0.008でした。
第1〜2週はRewardedの3つの工夫を実装した段階。Rewarded ARPDAUが0.012から0.024へ倍増。視聴完了率は42%から57%へ。一方でInterstitialの表示機会が減り、Interstitial ARPDAUは0.018から0.014に微減しました。トータルARPDAUは0.046。
第3〜5週はInterstitialの行動粒度キャップを導入。表示回数は減ったがCTRが上がり、Interstitial ARPDAUは0.014から0.020へ回復。同時にユーザー離脱率(セッション中の途中終了率)が23%から17%に改善。
第6〜7週はApp Openの漏れバケツルールを実装。App Open ARPDAUは0.008から0.011へ。総合ARPDAUは0.055まで上昇。
第8〜9週はAdGateの排他制御を入れて、3層の発火重複をゼロに。「広告が多すぎる」レビューが激減し、評価平均が3.8から4.1に改善。最終的にARPDAUは0.053(個別最適化フェーズの第7週から微減した週もありましたが、Day 30リテンションが上がったため、月次総収益は1.4倍)。
数値だけ見ると「ARPDAU 0.053は0.038の1.4倍ぴったり」ではありませんが、私が指標として重視しているのは「DAU × ARPDAU」の月次合計値です。DAUが安定して伸びたことで、月次広告収益は元の1.4倍に到達しました。
同じ設計で気をつけたい3つの失敗パターン
最後に、私が9週間でぶつかった3つの落とし穴と回避策を書き残します。
1つ目は「Rewardedの報酬を強くしすぎる」失敗です。広告視聴で48時間アンロックではなく「永久アンロック」にした週があり、その週のRewarded視聴回数は5倍に跳ね上がったのですが、翌週の視聴回数は元の30%まで落ちました。一度永久アンロックされてしまうと、ユーザーには「次にまた広告を見る理由」が無くなります。報酬は「再消費される設計」にしておくことを推奨します。
2つ目は「行動粒度キャップの閾値を、機械学習なしで決めようとした」ことです。最初は「8アクションで1回」と決め打ちしましたが、ユーザーごとに「8回が短すぎる人」と「8回でも長すぎる人」がいました。Firebase Remote Configで A/B テストを回して、リテンションに最も寄与する閾値を「ユーザーセグメント別に」設定する必要がありました。
3つ目は「AdGateを後から入れようとして、既存の広告呼び出しコードが散らばっていた」ことです。AdGateの導入時に「広告を直接呼ぶコード」が複数画面に散在していて、すべてをGate経由に書き換えるのに3日かかりました。新しいアプリでは、最初からAdGateを必須にする設計を入れておくべきでした。
ここまでが、9週間の運用ログです。3層連携の発想は、AdMobの公式ドキュメントには明示されていません。単一フォーマットを最大化する解説はあっても、3つの相互作用を扱った設計指針は、私が探した限りでは見つけられませんでした。個人開発者として12年やってきて、この「フォーマット間の調停」こそが、収益の天井を1段上げる鍵だと感じています。
同じように壁紙アプリやエンタメ系アプリで広告収益を運用している方の参考になれば嬉しいです。