Microsoftが開発したブラウザ自動操作・E2Eテストフレームワーク「Playwright(プレイライト)」は、実行速度の速さとテストの壊れにくさから、Webアプリケーションの品質保証や業務自動化(RPA)、AIエージェントのブラウザ操作基盤として幅広く活用されています。
一方で、実務で本格的に運用し始めると、「なぜ従来のSeleniumなどと比べてこれほど速く、複数ブラウザで安定して動くのか?」という裏側の仕組み(アーキテクチャ)への理解や、「並列実行したらデータが壊れた」「CI環境で急にタイムアウトや文字化けが起きた」「自動待機が効かないメソッドを踏んでしまった」といった特有の注意点・落とし穴への対策が欠かせなくなります。
本記事では、Playwrightの高速・安定動作を支える内部構造(WebSocket常時接続・3階層モデル・LocatorとActionabilityの判定フロー)から、開発・運用現場で必ず押さえておきたい7つの注意点と具体的な回避コードまで、図解とともに詳しく解説します。
あわせて読みたい(Playwrightの基本概要・初期導入ガイド)
Playwrightが誕生した歴史的背景や、コード自動生成(codegen)・Trace Viewer・UI Modeなどの便利機能、初期セットアップ手順については「Playwright(プレイライト)とは?誕生の背景やauto-waitの仕組み・便利機能・使い方を分かりやすく解説」もあわせてご覧ください。
Playwrightが高速・安定して動く内部仕組み(アーキテクチャ)
Playwrightが従来のブラウザ自動化ツールと決定的に異なるのは、単に「書きやすいAPIが用意されているから」ではなく、ブラウザエンジンと通信・制御する根本のアーキテクチャが現代のWebアプリ向けに再設計されているからです。まずはその裏側の仕組みを3つの視点から紐解いていきましょう。

WebSocketによる常時双方向通信とCDP・独自プロトコル
従来のSelenium(WebDriver方式)では、「要素を探す」「クリックする」「文字を入力する」といった1つひとつのコマンドを実行するたびに、テストプログラムから中継用のドライバ(ChromeDriverなど)へHTTPリクエストを送り、ドライバがブラウザに命令を伝えて結果をHTTPレスポンスとして返すという「都度接続・バケツリレー方式」をとっていました。
これに対しPlaywrightは、テスト開始時にブラウザと「WebSocket(ウェブソケット)」による1本の常時接続パイプを確立し、テストが終わるまでその接続を維持し続けます。
- 通信オーバーヘッドの大幅削減:コマンドごとにHTTP通信の接続・切断を繰り返さないため、数百ステップに及ぶ操作でもミリ秒単位で軽快に命令が伝わります。
- ブラウザ内部イベントのリアルタイム通知:双方向通信であるため、Playwright側から命令を送るだけでなく、ブラウザ側で発生した「DOM(Document Object Model)の書き換え」「ネットワーク通信(APIリクエストやHTTPレスポンスヘッダー)の完了」「コンソールエラーの発生」といった内部イベントが、リアルタイムでPlaywright側へプッシュ通知されます。
- 3大エンジンへの直接命令:Chromium系にはGoogleの「CDP(Chrome DevTools Protocol)」を使用し、FirefoxとWebKitに対しても、Playwrightチームが各ブラウザエンジンにパッチを当てて実装したCDP相当の専用デバッグプロトコルを用いて直接制御しています。
| 比較項目 | Selenium(WebDriver) | Cypress | Playwright |
|---|---|---|---|
| 通信・制御方式 | HTTP通信+ブラウザ別WebDriver中継 | ブラウザ内の同一JavaScriptループ内で実行 | WebSocket常時接続+DevToolsプロトコル直接制御 |
| 実行プロセス | ブラウザ外(Out-of-process) | ブラウザ内(In-process) | ブラウザ外(Out-of-process) |
| 複数タブ・複数オリジン | 対応(ウィンドウハンドル切替が必要) | 構造上の制約が多い(複数タブ非対応) | 標準対応(別タブ・ポップアップ・外部決済画面も自在) |
| イベント・通信の検知 | ポーリング(外から定期的に問い合わせ) | ブラウザ内フックで検知 | エンジン内部からイベント駆動で即時プッシュ通知 |
ブラウザ外プロセス(Out-of-process)から制御する構造の強み
上の表にある「ブラウザ外(Out-of-process)で動く」という点も、Playwrightの仕組みを理解するうえで非常に重要なポイントです。
たとえばCypressのように「テスト対象のWebページと同じブラウザ内のJavaScript空間(In-process)」で動くツールは、画面内のDOM要素に直接触れられる反面、ブラウザのセキュリティ制限(同一オリジンポリシーなど)をそのまま受けてしまいます。そのため、「ボタンを押したら別タブが開く」「外部のOAuth認証画面やクレジットカード決済画面へ遷移して戻ってくる」といったシナリオの再現が構造上苦手でした。
Playwrightは、ブラウザのすぐ外側にある独立したNode.jsプロセスから「ブラウザそのものを統括する司令塔」として命令を下します。だからこそ、複数タブや複数ウィンドウ、iframe、さらには「管理者としてログインしたブラウザ」と「一般ユーザーとしてログインしたブラウザ」を1つのテストコード内で同時に立ち上げてチャット通信させるようなテストも、何の制約もなく自然に記述できるのです。
複数言語(TypeScript・Python・Java・C#)が同じ品質で動く理由
PlaywrightはTypeScript / JavaScriptだけでなく、Python、Java、C#(.NET)の公式ライブラリを提供しています。「言語ごとに別々のチームが1から作っているの?」と思うかもしれませんが、実は全言語で共通の「Playwright Core(Node.js製のコアエンジン)」を内部に同梱する仕組みになっています。
PythonやJavaでPlaywrightのコードを実行すると、裏側で軽量なPlaywright Coreサーバが立ち上がり、各言語のプログラムはそのコアへパイプ通信で命令を渡します。この設計のおかげで、「Node.js版では動くのにPython版ではauto-waitの挙動が違う」といった言語間の品質差が生まれず、どの言語を選んでも同じ最新機能と安定性を享受できます。
高速な並列実行を支える「Browser・BrowserContext・Page」の3階層モデル
Playwrightでテストや自動化スクリプトを書くとき、内部では必ず「Browser(ブラウザ本体)」「BrowserContext(ブラウザコンテキスト)」「Page(ページ)」という3階層のオブジェクト構造が作られています。この3階層モデルこそが、Playwrightが何百件ものテストを数分で並列処理できる秘密です。

Browser(ブラウザ本体プロセス):最初に1度だけ起動する親エンジン
一番上の階層である Browser は、パソコン上で動くChromiumやFirefox、WebKitの実行プログラム(OSプロセス)そのものです。ブラウザ本体のプロセスをゼロから起動するには、メモリ確保や初期化処理のためにどうしても数秒間の時間と大きなCPU・メモリ負荷がかかります。そのため、Playwrightではテスト1件ごとに Browser を起動し直すことはしません。ワーカー(並列実行単位)ごとに最初に1回だけ起動し、使い回します。
BrowserContext(独立コンテキスト):数ミリ秒で作れる仮想シークレット空間
「1つのブラウザを使い回したら、前のテストでログインしたCookieやキャッシュが次のテストに残ってしまうのでは?」という問題を解決するのが、2階層目のBrowserContext(ブラウザコンテキスト)です。
BrowserContext は、普段私たちがChromeなどで使う「シークレットウィンドウ(プライベートブラウズ枠)」に相当する独立したメモリ空間です。
- 作成・破棄にかかる時間はわずか数ミリ秒:OSプロセスを立ち上げ直すのではなく、稼働中の
Browser内部に論理的な仕切りを作るだけなので、一瞬で生成・消去できます。 - Cookie・LocalStorage・キャッシュを分離:異なる
BrowserContext間ではログイン状態やストレージが一切共有されません。テスト1件ごとに新しいBrowserContextを作って終わったら捨てることで、毎回まっさらな初期状態からテストを実行できます。 - 端末設定も個別に指定可能:画面サイズ(Viewport)、User-Agent、タイムゾーン、位置情報(Geolocation)、ダークモード設定などもこの
BrowserContext単位で自由に設定できます。
Page(ページ・タブ):実際のクリックや入力を行う画面単位
一番下の3階層目にある Page は、BrowserContext の中に開かれた「1枚のブラウザタブ(またはポップアップウィンドウ)」を表します。私たちが普段テストコードで書く await page.goto(...) や await page.getByRole(...).click() はすべてこの Page オブジェクトに対する操作です。
// 3階層モデル(Browser > BrowserContext > Page)を明示的に扱うコード例
import { chromium } from 'playwright';
(async () => {
// 1. Browser(重い親プロセス)を1回だけ起動
const browser = await chromium.launch();
// 2. 独立した2つのBrowserContext(シークレット空間)を瞬時に作成
const adminContext = await browser.newContext();
const guestContext = await browser.newContext();
// 3. それぞれの空間の中にPage(操作タブ)を開く
const adminPage = await adminContext.newPage();
const guestPage = await guestContext.newPage();
// 管理者と一般ゲストの画面を、Cookieが混ざることなく同時に操作できる!
await adminPage.goto('https://example.com/admin');
await guestPage.goto('https://example.com/shop');
await browser.close();
})();
※普段 test('テスト名', async ({ page }) => { ... }) と書くときは、Playwrightのテストランナー(Fixture機能)がテスト開始前に自動で browser.newContext() と context.newPage() を実行し、テスト終了後に自動で破棄する処理を裏側で代行してくれています。
なぜ「sleep不要」で壊れないのか?LocatorとActionabilityの内部判定フロー
ReactやVue、Next.jsなどのモダンなフロントエンドでは、通信結果に応じて画面のDOM要素が何度も作り直されます。Playwrightが手動の sleep 待機なしでこうした動的画面を正確に操作できるのは、「Locator(ロケーター)の遅延評価」と「Actionability(アクション可能条件)の自動待機ループ」という2段構えの仕組みがあるからです。

要素参照をキャッシュしない「Locator(ロケーター)」の再評価アルゴリズム
従来のテストツールで非常に多かったエラーが、StaleElementReferenceError(古い要素参照エラー)です。これは「最初にボタン要素を取得して変数に入れたあと、画面の再レンダリングによって古いボタンがDOMから消えて新しいボタンに差し替わったため、変数の中の参照がリンク切れを起こしてクリックに失敗する」という現象です。
Playwrightの page.getByRole('button', { name: '保存' }) などが返す Locator オブジェクトは、画面上の要素そのものではなく、「どうやってその要素を見つけるかというレシピ(探索条件)」だけを保持しています。
// この時点ではまだブラウザ内のDOM検索は行われない(「探し方のレシピ」を作っただけ)
const saveButton = page.getByRole('button', { name: '保存' });
// 1回目のクリック:この瞬間にレシピを使って最新のDOMからボタンを探してクリック
await saveButton.click();
// (ここでReact等の再レンダリングが走り、DOM上のボタン要素が作り直されても大丈夫!)
// 2回目のクリック:もう一度レシピを使って最新のDOMから探し直すためエラーにならない
await saveButton.click();
操作直前に走る5つのActionability(アクション可能条件)チェック
そして .click() や .fill() などのアクションメソッドが呼ばれた瞬間、Playwrightの内部ではタイムアウト時間(デフォルト30秒)に達するまで、ブラウザエンジン内で以下の5つのステップを高速ループで検査します。
- 要素の解決(Resolved to Single Element):指定されたLocatorに一致する要素がDOM上に現れ、かつ「ただ1つ(厳密モード:Strict Mode)」に絞られているか。
- 可視性の確認(Visible):要素が空ではなく、
display: noneやvisibility: hiddenでもなく、画面上の描画ボックス(Bounding Box)を持っているか。 - 座標の静止確認(Stable):連続する2つ以上のアニメーションフレーム(
requestAnimationFrame)にわたって要素の座標が変化せず、スライド表示などのCSSアニメーションが完了してピタッと止まったか。 - イベント受信確認(Receives Events):クリックしようとしている中心座標にヒットテストを行い、ローディング用の半透明オーバーレイや別のモーダルが上に重なって邪魔をしていないか。
- 有効状態の確認(Enabled / Editable):対象のボタンに
disabled属性が付いていないか(文字入力の場合はさらにreadonlyでないか)。
万が一、ステップ4のヒットテストの瞬間に画面が書き換わって要素が消えた場合でも、Playwrightはエラーで止めず、自動的にステップ1の要素探索からやり直します。この緻密な内部ループがあるからこそ、通信速度やアニメーションに影響されない安定した自動操作が実現されています。
Web-First Assertions(自動リトライ検証)と通常の検証の違い
アクション操作と同じくらい重要なのが、結果を確かめるアサーション(expect)の仕組みです。Playwrightの expect(locator) には、条件が満たされるまでデフォルトで最大5秒間、何度も自動で再評価を繰り返す「Web-First Assertions(非同期リトライ検証)」が備わっています。
// × 避けるべき書き方:その瞬間の1回しか判定しないため、API通信中だとすぐ失敗する
const isVisible = await page.getByText('決済が完了しました').isVisible();
expect(isVisible).toBe(true);
// ○ 推奨される書き方(Web-First Assertions):表示されるまで最大5秒間自動でリトライ待機する
await expect(page.getByText('決済が完了しました')).toBeVisible();
Playwrightを実務・運用で使う際の7つの注意点と対策
このように非常に賢い仕組みを持つPlaywrightですが、その特性を知らずに従来のツールと同じ感覚でコードを書いたり運用したりすると、思わぬトラブルやセキュリティリスクを招くことがあります。実務で特に重要な7つの注意点と解決策を順番に確認していきましょう。

注意点:固定秒数待機(waitForTimeout)や脆いCSSセレクタの多用
最も多いアンチパターンが、画面の切り替わりを待つために await page.waitForTimeout(3000)(固定で3秒待つ)をテストのあちこちに入れてしまうことと、div > ul > li:nth-child(2) > .btn-blue のような構造依存のCSSセレクタを使ってしまうことです。
- なぜ問題なのか:
waitForTimeoutはデバッグ用の一時確認機能であり、本番のテストコードに残すと実行時間が遅くなるうえ、サーバーが少し重くて3.1秒かかった瞬間にテストが落ちます。また、見た目用のCSSクラス名やDOM階層はデザイン変更のたびに壊れます。 - 対策:待機は必ず「画面上に次の要素が表示されること(
expect(locator).toBeVisible())」や「特定のAPI通信が終わること(page.waitForResponse())」を条件にします。要素指定はgetByRole・getByLabel・getByTestIdを使いましょう。
// × NG例:固定秒数で待ち、デザインのクラス名で要素を探している
await page.locator('.search-box-input-v2').fill('ノートPC');
await page.locator('div.header > button.submit-btn').click();
await page.waitForTimeout(3000);
// ○ 対策例:RoleやLabelで指定し、結果の見出しが表示されるまで自動待機させる
await page.getByLabel('商品を検索').fill('ノートPC');
await page.getByRole('button', { name: '検索' }).click();
await expect(page.getByRole('heading', { name: '「ノートPC」の検索結果' })).toBeVisible();
注意点:並列実行(Parallel)によるデータベースや共有アカウントの競合
Playwrightはデフォルトで複数のワーカープロセスを立ち上げ、テストを同時並列で実行します。このとき、前に解説した BrowserContext によって「ブラウザ側の状態(Cookieなど)」は自動で分離されますが、「接続先のバックエンドサーバーやデータベース(DB)の状態」までは自動で分離されません。
- 起きやすい事故:全テストで同じ1つのテスト用アカウント(
test@example.com)を共有していると、ワーカーAが「カートに商品を1個入れて合計1個になるか検証するテスト」を実行している最中に、ワーカーBが「カートを空にするテスト」や「退会処理のテスト」を同時に走らせてしまい、ランダムにテストが失敗します。 - 対策:テストごと(またはワーカー番号
testInfo.parallelIndexごと)に独立したテストユーザーをAPI経由で動的生成して使うか、どうしても同じDBデータを書き換えるテスト群についてはtest.describe.configure({ mode: 'serial' })(直列実行)を指定して同時実行を避ける設計にします。
注意点:認証状態ファイル(storageState)のGitコミットによる情報漏洩
ログイン処理の時間を短縮するために認証状態を書き出す storageState(例:playwright/.auth/user.json)は非常に便利ですが、セキュリティ上の取り扱いに最大限の注意が必要です。
- なぜ危険なのか:出力されたJSONファイルの中には、ログイン済みの生きたセッションCookieやJWTアクセストークンが平文(そのまま読める文字)で保存されています。もしこのファイルを誤ってGitHubなどのリポジトリにコミット・プッシュしてしまうと、第三者がパスワードや二要素認証をバイパスしてアカウントに不正アクセスできてしまいます。
- 対策:認証ファイルの保存先ディレクトリ(
playwright/.auth/など)は、機能を使い始める最初の段階で必ず.gitignoreに追記し、Gitの追跡対象から外してください。
# .gitignore に必ず追加しておく設定例
playwright/.auth/
test-results/
playwright-report/
注意点:Playwright同梱のWebKitと実機iPhone・MacのSafariとの細かな差異
Playwrightの大きな魅力として「WindowsやLinuxマシン上でもApple Safari相当のWebKitエンジンでテストできる」という点がありますが、「オープンソースのWebKitエンジン」と「Apple製品の実機に搭載されているSafariアプリ」は100%同一ではないという点も理解しておく必要があります。
- 共通している部分(Playwrightで検証できる範囲):HTMLの解釈、CSSのレイアウト計算、JavaScript(JavaScriptCore)の動作、基本的なタッチイベントや画面サイズ再現など、Web表示の95%以上の挙動はWebKitエンジンで精度高く検証できます。
- 差異が出る部分(実機確認が推奨される範囲):iOS固有のシステムUI(ソフトウェアキーボードせり上がり時の特殊なアドレスバー伸縮)、Apple Payの決済シート、OS固有の商用動画コーデック(一部のDRM・HEVC再生)、カメラ・生体認証(Face ID / Touch ID)との連携などは実機OS層に依存します。決済や動画配信などOS依存度が高い機能は、Playwrightでの自動テストに加えてリリース前に実機Safari(またはBrowserStack等の実機クラウド)で最終確認を行うのが安全です。
注意点:CI環境(Docker・GitHub Actions)でのメモリ不足と日本語フォント文字化け
自分のノートPC(WindowsやMac)では問題なく通るテストが、GitHub ActionsやDockerコンテナ上のLinux環境(CIサーバー)で動かすと突然クラッシュしたり、スクリーンショットの日本語がすべて「□□□(豆腐)」に文字化けしたりすることがあります。
- 原因1(必要なOSライブラリ・日本語フォントの不足):素のUbuntuや軽量Dockerイメージには、ブラウザ描画に必要なシステムライブラリや日本語フォントが入っていません。
- 原因2(共有メモリ
/dev/shmやCPUの枯渇):CIサーバーは手元の開発PCよりもCPUコア数やメモリが少なく、Dockerコンテナのデフォルト共有メモリ(64MB)ではChromiumがメモリ不足でクラッシュすることがあります。 - 対策:ブラウザ導入時は必ず
--with-depsオプションを付けるか、Microsoft公式のDockerイメージ(mcr.microsoft.com/playwright)を使用します。また、CI環境ではワーカー数を控えめ(1〜2並列)に制限すると安定性が大きく向上します。
# CI環境(Linux)では --with-deps を付けることでOS依存ライブラリとフォントも一括導入される
npx playwright install --with-deps
# Docker実行時は --ipc=host を指定するとChromiumの共有メモリ不足クラッシュを防げる
docker run --init --ipc=host -it mcr.microsoft.com/playwright:v1.50.0-noble /bin/bash
注意点:要素のテキスト取得などで自動待機が働かない旧式メソッドの罠
「Playwrightはすべてのメソッドで自動的に待ってくれる」と思い込んでいると引っかかりやすいのが、一部の即時評価メソッドにはauto-wait(自動待機)が働かないという仕様です。
たとえば、要素のリストを配列として取得する locator.all() や、要素が存在するかどうかを真偽値で返す locator.isVisible()、locator.count()、さらにPuppeteer互換の古い page.$() / page.$$() などは、「呼ばれたその瞬間の画面状態」を0秒で即座に返してしまいます。
// × 注意:locator.all() や count() は要素が表示されるのを待たずに即座に「0件(空配列)」を返すことがある
const items = await page.getByRole('listitem').all();
// ○ 対策:先に expect().toHaveCount() などの自動待機検証を行うか、最初の要素の表示を待ってから取得する
await expect(page.getByRole('listitem').first()).toBeVisible();
const safeItems = await page.getByRole('listitem').all();
注意点:Webスクレイピングや外部サービス自動操作時の利用規約・負荷対策
Playwrightは自社のWebアプリのテストだけでなく、Pythonなどと組み合わせて日々のデータ収集(スクレイピング)や外部ブログ・管理画面への自動投稿ツールとしても広く使われています。自社管理外のWebサイトに対してPlaywrightを動かす際は、以下のルールとマナーを必ず守る必要があります。
- 利用規約と
robots.txtの確認:対象サイトの利用規約で自動プログラム(Bot)によるアクセスやスクレイピングが禁止されていないか、公式のREST APIが提供されていないかを最初に確認します(公式APIがある場合はAPI連携を優先するのが鉄則です)。 - 並列アクセスの制限と適切なリクエスト間隔:Playwrightは高速なため、ループ処理で何十ページも一斉に開くと相手サーバーにDoS攻撃のような高負荷を与えてしまい、IPアドレス遮断や業務妨害トラブルにつながる恐れがあります。外部サイト巡回時は必ず1並列(
workers: 1)にし、ページ遷移ごとに十分な待機間隔を空けてください。 - CAPTCHAや人間確認画面の扱い:reCAPTCHAやCloudflare Turnstileなどの認証ゲートが表示された場合、それをプログラムで無理に突破・回避しようとする行為は規約違反となります。定型業務の自動化では、ログインや認証ステップだけを人間が画面付き(ヘッドフルモード)で行い、セッションを引き継いで安全に処理する半自動設計が推奨されます。
まとめ
Playwrightの高速・安定動作を支える内部の仕組みから、実務運用で失敗しないための7つの注意点まで詳しく解説しました。最後に本記事の要点を整理します。
本記事のポイントまとめ
- 通信と制御の仕組み:操作ごとにHTTP通信を中継する従来方式とは異なり、WebSocketによる1本の常時双方向パイプとブラウザ外部(Out-of-process)からの直接プロトコル制御によって、高速かつ制約のない操作を実現している。
- 3階層の構造モデル:重い親プロセスである
Browserは1回だけ起動して共有し、テストごとには数ミリ秒で生成できる独立空間BrowserContext(シークレット枠相当)とPage(タブ)を切り替えることで、他テストと干渉しない高速な並列実行を可能にしている。 - LocatorとActionabilityの内部判定:
Locatorは操作の瞬間に毎回最新のDOMを再探索し、さらに「表示・静止・重なりなし・有効状態」など5つの条件が揃うまで自動待機するため、動的画面でも壊れにくい。 - 実務での重要注意点:固定秒数待機(
waitForTimeout)や即時評価メソッド(isVisible()/all())の罠を避け、並列実行時のDBデータ競合対策、storageState認証ファイルの.gitignore登録、CI環境での--with-deps導入、外部サイトへの負荷配慮を心がける。
「なぜPlaywrightは速くて壊れにくいのか」という裏側の仕組みを知っておくと、テストが失敗したときの原因切り分けが格段に速くなり、数多くのテストケースが増えても長く安定して動き続ける設計ができるようになります。ぜひ本記事の仕組みと注意点を、日々のE2Eテスト構築やブラウザ業務の自動化にお役立てください。
参考リンク・関連記事
- Playwright(プレイライト)とは?誕生の背景やauto-waitの仕組み・便利機能・使い方を分かりやすく解説(eguchi.net)
- DOM(ドキュメント・オブジェクト・モデル)とは?仕組みやツリー構造・JavaScriptでの操作例を分かりやすく解説(eguchi.net)
- 「HTTPレスポンスヘッダー」と「セキュリティヘッダー(HTTP Security Headers)」とは?仕組み・具体例・確認方法を分かりやすく解説(eguchi.net)
- Playwright 公式ドキュメント – Auto-waiting / Actionability
- Playwright 公式ドキュメント – Best Practices
