Cases / #56 · 2026-10-17 · Easy
A slow phone on a fast server: CPU throttling over a CDP session
Slow a remote Chromium's main thread 4× and 6× with Emulation.setCPUThrottlingRate, then time a JavaScript workload and a real page load — and see that it's per tab.
Run on production on 2026-10-17: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
Fleet browsers run on server CPUs, many times faster than the mid-range phone most of your users carry. A page that feels instant in a test can stall on that phone: long parsing, slow hydration, input that lags. Lighthouse's mobile run throttles the CPU 4× for exactly this reason. Can you do the same on a remote browser, and what does it slow down — and what not?
What we used, and why
| What | Why |
|---|---|
context.newCDPSession(page) | A raw CDP channel to the page over the normal Playwright connection (Chromium-based browsers) — no cdp: true needed. |
Emulation.setCPUThrottlingRate | DevTools' CPU throttling: rate: 4 makes the page's main thread run at a quarter of its speed. |
| A fixed JavaScript workload | Three million loop steps and a 200,000-item sort, timed with performance.now() inside the page; the same result every time (check), so only the speed changes. |
Navigation Timing + Performance.getMetrics | Response received, DOMContentLoaded and load from the page; main-thread busy time (TaskDuration) from Chromium. |
| A Wikipedia article | A real page with a big DOM (~9,400 nodes) and scripts to parse. |
How it works
- Launch headless Chromium behind a proxy.
- For rates 1×, 4× and 6×: new context, CDP session, set the rate, load the article, read its timings and main-thread time, run the workload.
- Finally throttle one tab 6× and run the workload in it and in an unthrottled tab next to it.
The code
The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:
CDPFLEET_API_KEY— your API key (dashboard → API keys)PROXY_URL— your proxy, e.g.http://user:[email protected]:8000
// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';
const KEY = process.env.CDPFLEET_API_KEY;
const RATES = [1, 4, 6];
const URL = 'https://en.wikipedia.org/wiki/Web_browser';
// A fixed amount of JavaScript work, timed inside the page: what a heavy client-side app does.
const WORK = `(() => {
const t = performance.now();
let h = 0;
for (let i = 0; i < 3e6; i++) h = (h * 31 + i) % 1000003;
const a = Array.from({ length: 200000 }, (_, i) => (i * 7919) % 100003).sort((x, y) => x - y);
return { ms: Math.round(performance.now() - t), check: h + a[1000] };
})()`;
const metrics = async (cdp) => Object.fromEntries((await cdp.send('Performance.getMetrics')).metrics.map((m) => [m.name, m.value]));
const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: 'new' }),
});
if (!res.ok) throw new Error(`launch ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
try {
const rows = [];
for (const rate of RATES) {
const context = await browser.newContext();
const page = await context.newPage();
// CPU throttling is a DevTools feature: reach it with a CDP session over the Playwright connection.
const cdp = await context.newCDPSession(page);
await cdp.send('Performance.enable');
await cdp.send('Emulation.setCPUThrottlingRate', { rate });
// A real page load: the network part isn't throttled, the main-thread work is.
await page.goto(URL, { waitUntil: 'load', timeout: 90000 });
const nav = await page.evaluate(() => {
const n = performance.getEntriesByType('navigation')[0];
return { response_ms: Math.round(n.responseEnd), dcl_ms: Math.round(n.domContentLoadedEventEnd), load_ms: Math.round(n.loadEventEnd) };
});
const m = await metrics(cdp);
const work = await page.evaluate(WORK);
rows.push({
rate,
work_ms: work.ms,
...nav,
parse_ms: nav.dcl_ms - nav.response_ms, // HTML fully received → DOMContentLoaded: parsing and scripts
main_thread_ms: Math.round(m.TaskDuration * 1000), // busy time while loading, from CDP
check: work.check,
});
await context.close();
}
// Is it per page? A throttled tab next to an unthrottled one in the same browser.
const context = await browser.newContext();
const slow = await context.newPage();
const fast = await context.newPage();
await (await context.newCDPSession(slow)).send('Emulation.setCPUThrottlingRate', { rate: 6 });
const sideBySide = { throttled_tab_ms: (await slow.evaluate(WORK)).ms, other_tab_ms: (await fast.evaluate(WORK)).ms };
await context.close();
console.log(JSON.stringify({ rows, side_by_side: sideBySide }, null, 2));
} finally {
await browser.close();
}
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import json
import os
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
RATES = [1, 4, 6]
URL = "https://en.wikipedia.org/wiki/Web_browser"
# A fixed amount of JavaScript work, timed inside the page: what a heavy client-side app does.
WORK = """(() => {
const t = performance.now();
let h = 0;
for (let i = 0; i < 3e6; i++) h = (h * 31 + i) % 1000003;
const a = Array.from({ length: 200000 }, (_, i) => (i * 7919) % 100003).sort((x, y) => x - y);
return { ms: Math.round(performance.now() - t), check: h + a[1000] };
})()"""
NAV = """() => {
const n = performance.getEntriesByType('navigation')[0];
return { response_ms: Math.round(n.responseEnd), dcl_ms: Math.round(n.domContentLoadedEventEnd), load_ms: Math.round(n.loadEventEnd) };
}"""
def metrics(cdp):
return {m["name"]: m["value"] for m in cdp.send("Performance.getMetrics")["metrics"]}
res = requests.post("https://starter.cdpfleet.com/chromium/session",
headers={"x-api-key": KEY},
json={"proxy": os.environ["PROXY_URL"], "headless": "new"}, timeout=60)
if not res.ok:
raise SystemExit(f"launch {res.status_code} {res.text}")
with sync_playwright() as p:
browser = p.chromium.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
try:
rows = []
for rate in RATES:
context = browser.new_context()
page = context.new_page()
# CPU throttling is a DevTools feature: reach it with a CDP session over the Playwright connection.
cdp = context.new_cdp_session(page)
cdp.send("Performance.enable")
cdp.send("Emulation.setCPUThrottlingRate", {"rate": rate})
# A real page load: the network part isn't throttled, the main-thread work is.
page.goto(URL, wait_until="load", timeout=90000)
nav = page.evaluate(NAV)
m = metrics(cdp)
work = page.evaluate(WORK)
rows.append({
"rate": rate,
"work_ms": work["ms"],
**nav,
"parse_ms": nav["dcl_ms"] - nav["response_ms"], # HTML fully received → DOMContentLoaded: parsing and scripts
"main_thread_ms": round(m["TaskDuration"] * 1000), # busy time while loading, from CDP
"check": work["check"],
})
context.close()
# Is it per page? A throttled tab next to an unthrottled one in the same browser.
context = browser.new_context()
slow = context.new_page()
fast = context.new_page()
context.new_cdp_session(slow).send("Emulation.setCPUThrottlingRate", {"rate": 6})
side_by_side = {"throttled_tab_ms": slow.evaluate(WORK)["ms"], "other_tab_ms": fast.evaluate(WORK)["ms"]}
context.close()
print(json.dumps({"rows": rows, "side_by_side": side_by_side}, indent=2))
finally:
browser.close()
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL
import com.google.gson.*;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.WaitUntilState;
import java.net.URI;
import java.net.http.*;
import java.util.*;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
static final int[] RATES = {1, 4, 6};
static final String URL = "https://en.wikipedia.org/wiki/Web_browser";
// A fixed amount of JavaScript work, timed inside the page: what a heavy client-side app does.
static final String WORK = """
(() => {
const t = performance.now();
let h = 0;
for (let i = 0; i < 3e6; i++) h = (h * 31 + i) % 1000003;
const a = Array.from({ length: 200000 }, (_, i) => (i * 7919) % 100003).sort((x, y) => x - y);
return JSON.stringify({ ms: Math.round(performance.now() - t), check: h + a[1000] });
})()""";
static final String NAV = """
JSON.stringify((() => {
const n = performance.getEntriesByType('navigation')[0];
return { response_ms: Math.round(n.responseEnd), dcl_ms: Math.round(n.domContentLoadedEventEnd), load_ms: Math.round(n.loadEventEnd) };
})())""";
static JsonObject json(Object s) { return JsonParser.parseString((String) s).getAsJsonObject(); }
static JsonObject rate(int rate) {
JsonObject o = new JsonObject();
o.addProperty("rate", rate);
return o;
}
public static void main(String[] args) throws Exception {
Gson gson = new GsonBuilder().serializeNulls().create();
String body = "{\"proxy\": " + gson.toJson(System.getenv("PROXY_URL")) + ", \"headless\": \"new\"}";
HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
JsonArray rows = new JsonArray();
for (int r : RATES) {
BrowserContext context = browser.newContext();
Page page = context.newPage();
// CPU throttling is a DevTools feature: reach it with a CDP session over the Playwright connection.
CDPSession cdp = context.newCDPSession(page);
cdp.send("Performance.enable");
cdp.send("Emulation.setCPUThrottlingRate", rate(r));
// A real page load: the network part isn't throttled, the main-thread work is.
page.navigate(URL, new Page.NavigateOptions().setWaitUntil(WaitUntilState.LOAD).setTimeout(90000));
JsonObject nav = json(page.evaluate(NAV));
double taskDuration = 0;
for (JsonElement m : cdp.send("Performance.getMetrics").getAsJsonArray("metrics"))
if (m.getAsJsonObject().get("name").getAsString().equals("TaskDuration")) taskDuration = m.getAsJsonObject().get("value").getAsDouble();
JsonObject work = json(page.evaluate(WORK));
JsonObject row = new JsonObject();
row.addProperty("rate", r);
row.add("work_ms", work.get("ms"));
for (Map.Entry<String, JsonElement> e : nav.entrySet()) row.add(e.getKey(), e.getValue());
// HTML fully received → DOMContentLoaded: parsing and scripts
row.addProperty("parse_ms", nav.get("dcl_ms").getAsLong() - nav.get("response_ms").getAsLong());
row.addProperty("main_thread_ms", Math.round(taskDuration * 1000)); // busy time while loading, from CDP
row.add("check", work.get("check"));
rows.add(row);
context.close();
}
// Is it per page? A throttled tab next to an unthrottled one in the same browser.
BrowserContext context = browser.newContext();
Page slow = context.newPage();
Page fast = context.newPage();
context.newCDPSession(slow).send("Emulation.setCPUThrottlingRate", rate(6));
JsonObject sideBySide = new JsonObject();
sideBySide.add("throttled_tab_ms", json(slow.evaluate(WORK)).get("ms"));
sideBySide.add("other_tab_ms", json(fast.evaluate(WORK)).get("ms"));
context.close();
JsonObject out = new JsonObject();
out.add("rows", rows);
out.add("side_by_side", sideBySide);
System.out.println(new GsonBuilder().serializeNulls().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
} finally {
browser.close();
}
}
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;
const string Url = "https://en.wikipedia.org/wiki/Web_browser";
int[] rates = [1, 4, 6];
// A fixed amount of JavaScript work, timed inside the page: what a heavy client-side app does.
const string Work = """
(() => {
const t = performance.now();
let h = 0;
for (let i = 0; i < 3e6; i++) h = (h * 31 + i) % 1000003;
const a = Array.from({ length: 200000 }, (_, i) => (i * 7919) % 100003).sort((x, y) => x - y);
return { ms: Math.round(performance.now() - t), check: h + a[1000] };
})()
""";
const string Nav = """
() => {
const n = performance.getEntriesByType('navigation')[0];
return { response_ms: Math.round(n.responseEnd), dcl_ms: Math.round(n.domContentLoadedEventEnd), load_ms: Math.round(n.loadEventEnd) };
}
""";
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
new { proxy = Environment.GetEnvironmentVariable("PROXY_URL"), headless = "new" });
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
var rows = new JsonArray();
foreach (var rate in rates)
{
var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();
// CPU throttling is a DevTools feature: reach it with a CDP session over the Playwright connection.
var cdp = await context.NewCDPSessionAsync(page);
await cdp.SendAsync("Performance.enable");
await cdp.SendAsync("Emulation.setCPUThrottlingRate", new() { ["rate"] = rate });
// A real page load: the network part isn't throttled, the main-thread work is.
await page.GotoAsync(Url, new() { WaitUntil = WaitUntilState.Load, Timeout = 90000 });
var nav = await page.EvaluateAsync<JsonElement>(Nav);
var metrics = (await cdp.SendAsync("Performance.getMetrics"))!.Value.GetProperty("metrics").EnumerateArray()
.ToDictionary(m => m.GetProperty("name").GetString()!, m => m.GetProperty("value").GetDouble());
var work = await page.EvaluateAsync<JsonElement>(Work);
var responseMs = nav.GetProperty("response_ms").GetInt64();
var dclMs = nav.GetProperty("dcl_ms").GetInt64();
rows.Add(new JsonObject
{
["rate"] = rate,
["work_ms"] = work.GetProperty("ms").GetInt64(),
["response_ms"] = responseMs,
["dcl_ms"] = dclMs,
["load_ms"] = nav.GetProperty("load_ms").GetInt64(),
["parse_ms"] = dclMs - responseMs, // HTML fully received → DOMContentLoaded: parsing and scripts
["main_thread_ms"] = Math.Round(metrics["TaskDuration"] * 1000), // busy time while loading, from CDP
["check"] = work.GetProperty("check").GetInt64(),
});
await context.CloseAsync();
}
// Is it per page? A throttled tab next to an unthrottled one in the same browser.
var sideContext = await browser.NewContextAsync();
var slow = await sideContext.NewPageAsync();
var fast = await sideContext.NewPageAsync();
await (await sideContext.NewCDPSessionAsync(slow)).SendAsync("Emulation.setCPUThrottlingRate", new() { ["rate"] = 6 });
var sideBySide = new JsonObject
{
["throttled_tab_ms"] = (await slow.EvaluateAsync<JsonElement>(Work)).GetProperty("ms").GetInt64(),
["other_tab_ms"] = (await fast.EvaluateAsync<JsonElement>(Work)).GetProperty("ms").GetInt64(),
};
await sideContext.CloseAsync();
var result = new JsonObject { ["rows"] = rows, ["side_by_side"] = sideBySide };
Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
await browser.CloseAsync();
}
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"os"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("CDPFLEET_API_KEY")
var rates = []int{1, 4, 6}
const url = "https://en.wikipedia.org/wiki/Web_browser"
// A fixed amount of JavaScript work, timed inside the page: what a heavy client-side app does.
const workJS = `(() => {
const t = performance.now();
let h = 0;
for (let i = 0; i < 3e6; i++) h = (h * 31 + i) % 1000003;
const a = Array.from({ length: 200000 }, (_, i) => (i * 7919) % 100003).sort((x, y) => x - y);
return JSON.stringify({ ms: Math.round(performance.now() - t), check: h + a[1000] });
})()`
const navJS = `JSON.stringify((() => {
const n = performance.getEntriesByType('navigation')[0];
return { response_ms: Math.round(n.responseEnd), dcl_ms: Math.round(n.domContentLoadedEventEnd), load_ms: Math.round(n.loadEventEnd) };
})())`
type work struct {
Ms int64 `json:"ms"`
Check int64 `json:"check"`
}
type nav struct {
ResponseMs int64 `json:"response_ms"`
DCLMs int64 `json:"dcl_ms"`
LoadMs int64 `json:"load_ms"`
}
type row struct {
Rate int `json:"rate"`
WorkMs int64 `json:"work_ms"`
ResponseMs int64 `json:"response_ms"`
DCLMs int64 `json:"dcl_ms"`
LoadMs int64 `json:"load_ms"`
ParseMs int64 `json:"parse_ms"`
MainThreadMs int64 `json:"main_thread_ms"`
Check int64 `json:"check"`
}
func launch(name string, options map[string]any) (map[string]any, error) {
body, _ := json.Marshal(options)
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
}
var session map[string]any
return session, json.NewDecoder(res.Body).Decode(&session)
}
// evalJSON runs a script that returns JSON.stringify(...) and decodes it into v.
func evalJSON(page playwright.Page, script string, v any) error {
s, err := page.Evaluate(script)
if err != nil {
return err
}
return json.Unmarshal([]byte(s.(string)), v)
}
func taskDuration(cdp playwright.CDPSession) (float64, error) {
result, err := cdp.Send("Performance.getMetrics", nil)
if err != nil {
return 0, err
}
for _, m := range result.(map[string]any)["metrics"].([]any) {
mm := m.(map[string]any)
if mm["name"] == "TaskDuration" {
switch v := mm["value"].(type) {
case float64:
return v, nil
case int:
return float64(v), nil
}
}
}
return 0, nil
}
func measure(browser playwright.Browser, rate int) (row, error) {
r := row{Rate: rate}
context, err := browser.NewContext()
if err != nil {
return r, err
}
defer context.Close()
page, err := context.NewPage()
if err != nil {
return r, err
}
// CPU throttling is a DevTools feature: reach it with a CDP session over the Playwright connection.
cdp, err := context.NewCDPSession(page)
if err != nil {
return r, err
}
if _, err := cdp.Send("Performance.enable", nil); err != nil {
return r, err
}
if _, err := cdp.Send("Emulation.setCPUThrottlingRate", map[string]any{"rate": rate}); err != nil {
return r, err
}
// A real page load: the network part isn't throttled, the main-thread work is.
if _, err := page.Goto(url, playwright.PageGotoOptions{WaitUntil: playwright.WaitUntilStateLoad, Timeout: playwright.Float(90000)}); err != nil {
return r, err
}
var n nav
if err := evalJSON(page, navJS, &n); err != nil {
return r, err
}
td, err := taskDuration(cdp)
if err != nil {
return r, err
}
var w work
if err := evalJSON(page, workJS, &w); err != nil {
return r, err
}
r.WorkMs, r.Check = w.Ms, w.Check
r.ResponseMs, r.DCLMs, r.LoadMs = n.ResponseMs, n.DCLMs, n.LoadMs
r.ParseMs = n.DCLMs - n.ResponseMs // HTML fully received → DOMContentLoaded: parsing and scripts
r.MainThreadMs = int64(math.Round(td * 1000)) // busy time while loading, from CDP
return r, nil
}
func run() (map[string]any, error) {
session, err := launch("chromium", map[string]any{"proxy": os.Getenv("PROXY_URL"), "headless": "new"})
if err != nil {
return nil, err
}
pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
if err != nil {
return nil, err
}
defer pw.Stop()
browser, err := pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
if err != nil {
return nil, err
}
defer browser.Close()
rows := []row{}
for _, rate := range rates {
r, err := measure(browser, rate)
if err != nil {
return nil, err
}
rows = append(rows, r)
}
// Is it per page? A throttled tab next to an unthrottled one in the same browser.
context, err := browser.NewContext()
if err != nil {
return nil, err
}
defer context.Close()
slow, _ := context.NewPage()
fast, _ := context.NewPage()
cdp, err := context.NewCDPSession(slow)
if err != nil {
return nil, err
}
if _, err := cdp.Send("Emulation.setCPUThrottlingRate", map[string]any{"rate": 6}); err != nil {
return nil, err
}
var s, f work
if err := evalJSON(slow, workJS, &s); err != nil {
return nil, err
}
if err := evalJSON(fast, workJS, &f); err != nil {
return nil, err
}
return map[string]any{"rows": rows, "side_by_side": map[string]int64{"throttled_tab_ms": s.Ms, "other_tab_ms": f.Ms}}, nil
}
func main() {
out, err := run()
if err != nil {
log.Fatal(err)
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
enc.Encode(out)
fmt.Print(buf.String())
}
What we got
| CPU slowdown | JS workload (ms) | HTML → DOMContentLoaded (ms) | Main-thread busy (ms) | HTML received (ms) | Load event (ms) |
|---|---|---|---|---|---|
| 1× | 77 | 486 | 422 | 2272 | 4615 |
| 4× | 327 | 685 | 1448 | 1075 | 3230 |
| 6× | 497 | 805 | 2110 | 451 | 2398 |
| 6×, one tab only | 494 (other tab: 126) |
From the Node.js run on 2026-10-17. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.
Raw output (Node.js)
{
"rows": [
{
"rate": 1,
"work_ms": 77,
"response_ms": 2272,
"dcl_ms": 2758,
"load_ms": 4615,
"parse_ms": 486,
"main_thread_ms": 422,
"check": 488537
},
{
"rate": 4,
"work_ms": 327,
"response_ms": 1075,
"dcl_ms": 1760,
"load_ms": 3230,
"parse_ms": 685,
"main_thread_ms": 1448,
"check": 488537
},
{
"rate": 6,
"work_ms": 497,
"response_ms": 451,
"dcl_ms": 1256,
"load_ms": 2398,
"parse_ms": 805,
"main_thread_ms": 2110,
"check": 488537
}
],
"side_by_side": {
"throttled_tab_ms": 494,
"other_tab_ms": 126
}
}Takeaways
- One CDP call per page, no
cdp: true:newCDPSessionover the ordinary Playwright connection reaches DevTools' CPU throttling, the same knob Lighthouse's mobile run turns. - The rate is honest for JavaScript: the same workload (same result every time) ran 3.3–4.8× slower at 4× and 4.6–6.5× slower at 6× across the five runs.
- It slows the page's work, not its network. At 6× the main thread was busy 3.4–7.0× longer while loading the article, and getting from the received HTML to DOMContentLoaded took 1.7–4.6× longer. The load event didn't follow — 1.9–4.9 s at every rate — because downloading through the proxy dominated it and isn't throttled. Measure main-thread time and parse time, not the load event; for a slow phone on a slow network, add network throttling (case 36).
- Throttling is per tab: with one tab at 6×, the workload took 382–502 ms there and 67–126 ms in the tab next to it, in the same browser. Set the rate on every page you open, and you can run a throttled and a full-speed profile in one session.
- Compare ratios, not raw times: the unthrottled workload took 65–84 ms on whichever machine ran the session, so a 4× run on one machine and a 1× run on another don't compare. Take the baseline in the same session. Core Web Vitals from the same kind of run are in case 55; CDP sessions need a Chromium-based browser.