- 前置き「最近QRコードを活用したサービス増えましたね!」
- Screen Wake Lock API
- Screen Wake Lock API の挙動検証パターン
- 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を取得する・リリースする ようなコードを書いたことがあれば似たような流れとなります。
navigator.wakeLock.request('screen')メソッドで、画面OFFを抑制するリクエストを送ります。今の所引数に取れるのはscreenのみのようです。- 上記レスポンスとして、画面OFFの抑制を解除するためのオブジェクトを取得できます。
- 画面OFFの抑制が不要になったタイミングで、2.で取得したオブジェクトに実装されている
release()メソッド を呼び出すことで再度画面が端末の設定に応じて制御されるようになります。
このあと実際の端末で挙動を確かめるために、検証アプリを実装します。 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を使いこなした開発フローや、このような地道な検証にも面白みを感じられるエンジニアの皆さんを絶賛募集中です。ご興味が湧いてきましたら、ぜひ一読ください。


