常に画面がONになって欲しい瞬間のあるWebアプリへ贈る Screen Wake Lock API

こんにちは、プラットフォームサービス開発部の中村です。

最近はエンジニアリングマネージャー 兼 技術広報室長として、プロダクトを開発するチームを技術・人目線でサポートすることをお仕事としています。ただ今回はテックブログアドベントカレンダーということで、技術に関する記事でいきます!

この記事は OPTiM TECH BLOG Advent Calendar 2025 Day 21 の記事です、いつか役に立つ後学として読みいただければ幸いです。

前置き「最近QRコードを活用したサービス増えましたね!」

ここ数年、QRコードを利用したWebサービスがかなり増えている印象です。個人的な感覚値ですが、PayPayの普及によりQRコードを使う機会がかなり増え、それに合わせて決済だけでなくチケット周りでの普及が特に進んでいる印象があります。

しかし、同時にこんなこともありませんか?

  • 👮‍♀️ 案内員さん「QRコードをすぐに出せるように、あらかじめ準備をお願いしますー! スクリーンショットのQRコードは無効ですー!」
  • 👨 自分(チケットページを開いて準備しておこう)
  • 🕰️ ... 1分後 🕰️
  • 👮‍♀️ 案内員さん「QRコードのご提示お願いします」
  • 👨 自分「はい.... (あ、画面がスリープしてた)、すみません」

気の利いたネイティブアプリでは、QRコードが表示されると スマホの画面の明るさ最大にして、画面を常時オンにしてくれるものはよく見ますが、Webアプリではそういった対応がされていないものも見かけます。

その画面OFFの抑制、 Webアプリでもできます! しかも、 2025年3月以降のバージョンの主要なブラウザ ほぼ全て で実装されています! ということで、そんな時に使っておきたい Screen Wake Lock APIを実際の挙動の確認も交えてご紹介します。

Screen Wake Lock API

MDNにドキュメントがありますので細かい話はカットしますが、とても簡単に利用できます。非同期な処理の中で同時に共有リソースへのアクセスを抑制するために Lockを取得する・リリースする ようなコードを書いたことがあれば似たような流れとなります。

  1. navigator.wakeLock.request('screen') メソッドで、画面OFFを抑制するリクエストを送ります。今の所引数に取れるのは screen のみのようです。
  2. 上記レスポンスとして、画面OFFの抑制を解除するためのオブジェクトを取得できます。
  3. 画面OFFの抑制が不要になったタイミングで、2.で取得したオブジェクトに実装されている release() メソッド を呼び出すことで再度画面が端末の設定に応じて制御されるようになります。

developer.mozilla.org

このあと実際の端末で挙動を確かめるために、検証アプリを実装します。 Screen Wake Lock API部分は、LLM (ClaudeCode) を使ってReact Hookとして実装してもらいました。電気がついている間は、画面がOFFにならないはずです。

実装した検証用アプリ

useWakeLock.tsx

"use client";

import { useState, useEffect, useRef, useCallback } from "react";

type UseWakeLockReturn = [
  Error | null,
  boolean,
  (lock: boolean) => void,
];

export default function useWakeLock(): UseWakeLockReturn {
  const [isLocked, setIsLocked] = useState(false);
  const [lockError, setLockError] = useState<Error | null>(null);
  const wakeLockRef = useRef<WakeLockSentinel | null>(null);

  const requestWakeLock = useCallback(async () => {
    if (!("wakeLock" in navigator)) {
      setLockError(new Error("Wake Lock API is not supported in this browser"));
      return;
    }

    try {
      wakeLockRef.current = await navigator.wakeLock.request("screen");
      setIsLocked(true);
      setLockError(null);

      wakeLockRef.current.addEventListener("release", () => {
        console.log("Wake Lock was released");
      });
    } catch (err) {
      setLockError(err instanceof Error ? err : new Error(String(err)));
      setIsLocked(false);
    }
  }, []);

  const releaseWakeLock = useCallback(async () => {
    if (wakeLockRef.current) {
      try {
        await wakeLockRef.current.release();
        wakeLockRef.current = null;
        setIsLocked(false);
        setLockError(null);
      } catch (err) {
        setLockError(err instanceof Error ? err : new Error(String(err)));
      }
    }
  }, []);

  const setLock = useCallback(
    (lock: boolean) => {
      if (lock) {
        requestWakeLock();
      } else {
        releaseWakeLock();
      }
    },
    [requestWakeLock, releaseWakeLock],
  );

  useEffect(() => {
    const handleVisibilityChange = () => {
      if (document.visibilityState === "visible" && isLocked) {
        requestWakeLock();
      }
    };

    document.addEventListener("visibilitychange", handleVisibilityChange);

    return () => {
      document.removeEventListener("visibilitychange", handleVisibilityChange);
    };
  }, [isLocked, requestWakeLock]);

  useEffect(() => {
    return () => {
      if (wakeLockRef.current) {
        wakeLockRef.current.release();
        wakeLockRef.current = null;
      }
    };
  }, []);

  return [lockError, isLocked, setLock];
}

page.tsx

"use client";

import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import useWakeLock from "./hooks/useWakeLock";

export default function Home() {
  const [lockError, isLocked, setLock] = useWakeLock();

  return (
    <div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
      <main
        className={`flex min-h-screen w-full flex-col items-center justify-between py-32 px-16 transition-all duration-500 ${
          isLocked
            ? "bg-yellow-50 shadow-[inset_0_0_100px_rgba(255,255,200,0.3)]"
            : "bg-cyan-950"
        } dark:bg-black sm:items-start`}
      >
        <Stack className="w-full" spacing={2}>
          <p
            className={`text-9xl self-center transition-all duration-300 ${
              isLocked
                ? "drop-shadow-[0_0_30px_rgba(255,255,10,0.8)] opacity-100"
                : "opacity-15"
            }`}
          >
            💡
          </p>
          <Button
            variant="outlined"
            onClick={() => setLock(!isLocked)}
            sx={
              isLocked
                ? { borderColor: "#003444", color: "#003444" }
                : { borderColor: "#f0ded4", color: "#f0ded4" }
            }
            className="self-center transition-all duration-300"
          >
            {isLocked ? "Turn off" : "Always on"}
          </Button>
          <p className="text-9xl self-center transition-all duration-300">
            {isLocked ? "😳" : "😴"}
          </p>
          <p className="text-red-400">{lockError?.message}</p>
        </Stack>
      </main>
    </div>
  );
}

Screen Wake Lock API の挙動検証パターン

特にAndroidのネイティブアプリを開発されたことがある人は経験があるかもしれませんが、 「OSやブラウザが対応しているからといっても、デバイス側がきちんと対応していないと機能しないこともある」 ため、実際に動作確認をしておこうと思います。また、そもそもデバイス側が省電力モードなどを適応している場合には、例外を返す可能性があるとのことなのでその点も検証しておこうと思います。対象は以下のデバイス・ブラウザです。 また、電源周りに関しては以下の条件でそれぞれ確認しました。

  • OSによる省電力モードがONの状態 / OFFの状態
  • 電源で駆動している状態 / バッテリーで駆動している状態
機種 OSバージョン ブラウザ ブラウザバージョン 省電力モード 電源
Galaxy A23 5G (Android) Android 14 (One UI 6.1) Google Chrome 143.0.7499.52 ON
OFF
AC
バッテリー
Galaxy A23 5G (Android) Android 14 (One UI 6.1) Firefox 146.0.2 ON
OFF
AC
バッテリー
iPhone 13 (iOS) iOS 18.1 Safari 18.1 ON
OFF
AC
バッテリー
iPhone 13 (iOS) iOS 18.1 Google Chrome 143.0.7499.92 ON
OFF
AC
バッテリー
iPhone 13 (iOS) iOS 18.1 Firefox 146.1 ON
OFF
AC
バッテリー
Thinkpad Windows 11 25H2 Microsoft Edge 143.0.3650.75 ON
OFF
AC
バッテリー
Thinkpad Windows 11 25H2 Google Chrome 143.0.7499.41 ON
OFF
AC
バッテリー
Thinkpad Windows 11 25H2 Firefox 146.0 ON
OFF
AC
バッテリー
Macbook Air M2 (2022) macOS 15.3.1 (Sequoia) Safari 18.3 ON
OFF
AC
バッテリー
Macbook Air M2 (2022) macOS 15.3.1 (Sequoia) Google Chrome 143.0.7499.110 ON
OFF
AC
バッテリー
Macbook Air M2 (2022) macOS 15.3.1 (Sequoia) Firefox 146.0 ON
OFF
AC
バッテリー

省電力モードはOS標準のものを利用して検証しました、サードパーティー製のものは未検証です。

macOS

iPhone

Windows

Android

Screen Wake Lock API の挙動検証結果

以下に検証結果をまとめます、結果は以下の絵文字としています。 余談ですが、弊社のサービスの1つである OPTiM Biz も多彩なOSへ対応するための動作確認を実端末を使って行っているため、社内に大量の端末があります。(サービス自体がMDMという特性もあるため、実機での検証が必要な場面が多く存在します)

  • ✅ : .request() および .release()が正常に動作した
  • ❌: .request()を呼び出した際に例外が発生した
  • ⚠️: 上記以外の挙動(補足を記載)
# OS ブラウザ 省電力モード 電源 結果
1 Android Google Chrome OFF AC
2 Android Google Chrome ON AC
3 Android Google Chrome OFF バッテリー
4 Android Google Chrome ON バッテリー
5 Android Firefox OFF AC
6 Android Firefox ON AC
7 Android Firefox OFF バッテリー
8 Android Firefox ON バッテリー
9 iOS Safari OFF AC
10 iOS Safari ON AC
11 iOS Safari OFF バッテリー
12 iOS Safari ON バッテリー
13 iOS Google Chrome OFF AC
14 iOS Google Chrome ON AC
15 iOS Google Chrome OFF バッテリー
16 iOS Google Chrome ON バッテリー
17 iOS Firefox OFF AC
18 iOS Firefox ON AC
19 iOS Firefox OFF バッテリー
20 iOS Firefox ON バッテリー
21 Windows Google Chrome OFF AC
22 Windows Google Chrome ON AC
23 Windows Google Chrome OFF バッテリー
24 Windows Google Chrome ON バッテリー
25 Windows Microsoft Edge OFF AC
26 Windows Microsoft Edge ON AC
27 Windows Microsoft Edge OFF バッテリー
28 Windows Microsoft Edge ON バッテリー
29 Windows Firefox OFF AC
30 Windows Firefox ON AC
31 Windows Firefox OFF バッテリー
32 Windows Firefox ON バッテリー
33 macOS Safari OFF AC
34 macOS Safari ON AC
35 macOS Safari OFF バッテリー
36 macOS Safari ON バッテリー
37 macOS Google Chrome OFF AC
38 macOS Google Chrome ON AC
39 macOS Google Chrome OFF バッテリー
40 macOS Google Chrome ON バッテリー
41 macOS Firefox OFF AC
42 macOS Firefox ON AC
43 macOS Firefox OFF バッテリー
44 macOS Firefox ON バッテリー

以下、そのほか細かい挙動の補足です。手元のmacOSでのみ確認しています。

  • Screen Look Requestを実行した ブラウザのタブ を非アクティブにした(別タブに切り替えた)場合 => 画面OFFになった
    • 再度、ブラウザの該当タブをアクティブにしたところ、画面OFFが抑制された
  • Screen Look Requestを実行した ブラウザのウィンドウ を最小化した場合 => 画面OFFが抑制されたまま
  • Screen Look Requestを実行した ブラウザのウィンドウ が非アクティブな場合 => 画面OFFが抑制されたまま

結果に対する所感など

MDNの説明にもある通り、バッテリー節約モードを適応した場合には例外が発生するかと思っていましたが、2025/12/16時点の状態では特にそういった事象は見受けられず、Screen Wake Lock API による画面OFFの抑制の方が優先されるようです。恐らく想定されているユースケース自体が何時間も画面を点灯させるものではなく、ごく数分から数十分程度を想定させているからでしょうか。思っていたよりも強力に効くものだなという印象でした。

request したものは release してあげないと、OSによる画面OFF・画面ロックポリシーが働かないのでセキュリティ的によくない状況にもなりうるため、きちんと考慮した実装と検証は必要に感じました。

色々とうまくScreen Wake Lock API が機能しないパターンを探そうとしてみたのですが、あらゆる場合で機能してしまったので navigator.wakeLock.request('screen') メソッド呼び出し時に例外が発生したパターンがあれば教えて欲しいくらいです...。

※謝辞
検証手伝っていただいたチームの皆様、ありがとうございました。

まとめ

今回は Screen Wake Lock API に関して、実機による動作検証も含めて挙動を確認しました。また実機での動作確認も含めて、想像していたよりもあらゆるユースケースにおいて機能するという点も分かりました。 実装も比較的容易ですし、こういったAPIの存在とシュッとできることを知っておくことで1stローンチ時から ちょっと気の利いてる アプリとしてリリースできるので認知していることは大事だと思います。また最近では Spec Kit などを用いたコーディングエージェントを利用した開発フローの研究・実践も進んでいますので、LLMを使いこなすためにも人間側の も重要だなと思うこの頃です。

LLMを使いこなした開発フローや、このような地道な検証にも面白みを感じられるエンジニアの皆さんを絶賛募集中です。ご興味が湧いてきましたら、ぜひ一読ください。

www.optim.co.jp