How to Fix Low-FPS Chrome DevTools Protocol Screencasts
Diagnose low-FPS CDP screencasts, fix backpressure and encoding, preserve timestamps, and choose the right Chrome recording API.
Low FPS in a Chrome DevTools Protocol (CDP) screencast is usually a pipeline problem. The page may be rendering slowly, the host may be CPU or GPU constrained, your client may be delaying Page.screencastFrameAck, image encoding or decoding may be the bottleneck, or the video assembler may be using incorrect timestamps. Measure each stage before changing a nominal FPS setting.
For individual frames, use Page.startScreencast. Set a bounded image format and size, acknowledge every frame promptly, and keep protocol I/O separate from disk writes or decoding. If you need a playable recording stream, evaluate Page.startScreenRecording instead. Preserve the timestamp attached to every frame when you assemble a video.
What “low FPS” can mean
Separate these measurements:
| Measurement | What it tells you |
|---|---|
| Page render FPS | Whether the page can produce frames at the desired rate. |
| Delivered frame rate | How many screencastFrame events arrive after CDP throttling and frame dropping. |
| Acknowledgement latency | Whether your consumer is creating backpressure. |
| Encode/decode time | Whether PNG/JPEG/WebP processing is consuming the available CPU. |
| Transport throughput | Whether WebSocket bandwidth or disk writes prevent timely processing. |
| Playback FPS and duration | Whether your muxer used real capture timestamps instead of a fixed nominal rate. |
Do not call everyNthFrame or a requested frame rate an achieved FPS. Record frame count and elapsed wall-clock time for every run.
1. Establish a baseline
- Capture a minimal static page with the same browser and host.
- Capture the real page with screencasting disabled to measure page-only performance.
- Record start and stop times, received frame count, frame dimensions, encoded bytes, acknowledgement latency, decode time and final video duration.
- Repeat with a lower resolution and JPEG to determine whether capture or image processing is limiting throughput.
const received = [];
const started = performance.now();
// For each screencastFrame event:
received.push({
timestamp: params.metadata.timestamp,
receivedAt: performance.now(),
bytes: Buffer.byteLength(params.data, 'base64')
});
// At stop:
const elapsedSeconds = (performance.now() - started) / 1000;
console.log({
frames: received.length,
elapsedSeconds,
deliveredFps: received.length / elapsedSeconds,
bytes: received.reduce((n, f) => n + f.bytes, 0)
});
2. Profile the page and host
Open Chrome DevTools Performance and inspect FPS, CPU, frame, paint, layer and scrolling tracks. In the Rendering drawer, enable frame rendering statistics, paint flashing, layer borders or scrolling-performance diagnostics when they match the suspected problem. Sustained CPU saturation means you should reduce page work before tuning CDP: simplify animations, reduce layout churn, pause unnecessary timers, remove expensive filters and lower the amount of content rendered in the viewport.
Chrome’s performance guidance uses 60 FPS as the smooth-animation target and recommends investigating CPU and individual frames when rendering misses that target: Chrome DevTools Performance documentation.
3. Tune Page.startScreencast
The CDP Page domain exposes these controls:
| Option | Effect | Practical use |
|---|---|---|
format |
Image encoding, such as JPEG or PNG. | Use JPEG for photographic or animated content; PNG preserves lossless edges but is often larger and slower. |
quality |
Lossy image quality, where supported. | Set a bounded value and measure bytes and decode time. |
maxWidth/maxHeight |
Bounds delivered frame dimensions. | Lower these when encoding, transport or decoding cannot keep up. |
everyNthFrame |
Requests delivery of every Nth produced frame. | Use only when dropping frames is acceptable. It cannot make a slow page render faster. |
maxFramesInFlight |
Limits frames that can be outstanding. | Align it with consumer capacity; a large queue increases memory and latency. |
sendFrameMeta |
Includes frame metadata. | Keep metadata when assembling a correctly timed recording. |
captureBeyondViewport |
Controls capture beyond the visible viewport. | Disable unnecessary beyond-viewport work for a viewport-only recording. |
See the Page.startScreencast protocol reference for the current parameter definitions.
4. Acknowledgement and backpressure
Every Page.screencastFrame event carries a session ID. Send Page.screencastFrameAck for that ID immediately. Do not wait for a slow image decoder, a video encoder or a disk write before acknowledging. Copy the base64 payload and metadata into a bounded worker queue, acknowledge on the protocol loop, and define a policy for overload: drop old frames, drop new frames, or stop capture.
import CDP from 'chrome-remote-interface';
import { writeFile, mkdir } from 'node:fs/promises';
const client = await CDP({ port: 9222 });
const { Page } = client;
await mkdir('./frames', { recursive: true });
let index = 0;
let outstanding = 0;
const maxQueue = 8;
const queue = [];
Page.screencastFrame(async (event) => {
outstanding += 1;
const item = {
data: event.data,
timestamp: event.metadata?.timestamp,
sessionId: event.sessionId,
index: index++
};
// Acknowledge before any potentially slow work.
await Page.screencastFrameAck({ sessionId: event.sessionId });
outstanding -= 1;
if (queue.length >= maxQueue) {
queue.shift(); // Explicit overload policy: drop the oldest queued frame.
}
queue.push(item);
console.log({ queue: queue.length, outstanding, timestamp: item.timestamp });
});
await Page.startScreencast({
format: 'jpeg',
quality: 75,
maxWidth: 1280,
maxHeight: 720,
maxFramesInFlight: 2,
everyNthFrame: 1,
sendFrameMeta: true
});
await new Promise(resolve => setTimeout(resolve, 10000));
await Page.stopScreencast();
await client.close();
The example demonstrates the acknowledgement order and queue instrumentation. In production, replace the queue placeholder with a worker that decodes or encodes frames and stores each frame’s timestamp.
5. Choose PNG, JPEG and dimensions deliberately
- JPEG: usually the first choice for video-like content because payloads are smaller. Measure quality loss around text, sharp borders and animation.
- PNG: useful for lossless UI, diagrams and transparency, but can increase CPU, memory, transport and disk pressure.
- WebP: consider it when your decoder and downstream tooling support it; verify the CDP version and your muxing path.
- Resolution: halve width and height for a diagnostic run. If FPS recovers, capture cost is material. Restore resolution gradually.
- Quality: lower quality until the consumer keeps up, then raise it while watching visual artifacts and end-to-end latency.
everyNthFrame is a sampling control, not a frame-rate guarantee. A Chrome DevTools Protocol issue reports that slow pages can still average below 24 FPS even when this option is used: devtools-protocol issue #63.
6. Keep the protocol loop non-blocking
- Use a separate worker for base64 decoding, image transforms and video encoding.
- Use bounded queues and expose queue depth, dropped frames and acknowledgement latency in logs.
- Avoid synchronous filesystem calls in the event handler.
- Do not retain every full-resolution frame without a memory limit.
- Keep one capture session per tab when possible; competing CDP consumers can add scheduling and transport pressure.
The maxFramesInFlight setting should reflect what the consumer can process. A larger value may improve burst tolerance but increases memory and can make latency worse. The protocol’s sendLastFrame behavior is also a latency trade-off: retaining the last produced frame can improve freshness while reducing overall performance. Confirm the exact semantics in the Page domain specification.
7. Use Page.startScreenRecording when a stream is the real requirement
Page.startScreenRecording is designed for a recording stream rather than a consumer-managed sequence of individual screencast images. It accepts a maximum frame rate and dimensions and returns an IO stream. Choose it when you need a direct recording and do not need per-frame overlays, selective dropping or a custom encoder. Choose Page.startScreencast when you need individual compressed frames and full control over acknowledgement and assembly.
Read the current method and stream details in the Page.startScreenRecording reference. Treat the maximum frame rate as a cap, not a promise: page rendering and host capacity still determine the achieved rate.
8. Assemble video from capture timestamps
Preserve metadata.timestamp for every frame and pass it into your muxer or timestamp-aware encoder. Do not assign every frame a fixed interval such as 1/25 second unless that is an intentional approximation.
A 2025 Chrome DevTools MCP report describes output approximately 2.5–2.6 times longer than real elapsed capture when an assembler assumed a fixed nominal 25 FPS: Chrome DevTools MCP issue #2204. That report is evidence of a timestamp-assembly bug pattern, not a universal CDP rule. If playback is slow while capture metrics look healthy, inspect the muxer’s time base, frame duration, variable-frame-rate support and timestamp units before changing browser settings.
9. A complete diagnostic sequence
- Baseline: static page, real page, and screencast disabled.
- Profile: inspect DevTools CPU and FPS tracks and host CPU/GPU usage.
- Reduce capture cost: JPEG, bounded quality, smaller dimensions.
- Fix backpressure: acknowledge immediately, instrument outstanding frames and bound queues.
- Validate timestamps: compare first-to-last metadata time with final video duration.
- Choose the API: direct recording stream for simple files; screencast events for custom processing.
- Restore quality: increase dimensions and quality one variable at a time while recording achieved FPS.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Frames stop arriving | Missing or delayed acknowledgements. | Acknowledge every session ID promptly and log acknowledgement latency. |
| High CPU and large payloads | PNG or excessive resolution. | Try JPEG, lower quality and cap dimensions. |
| Low FPS only on the real site | Page rendering, layout or animation workload. | Profile the page; reduce animation and layout work before CDP tuning. |
| Memory grows continuously | Unbounded frame queue or retained buffers. | Use a bounded queue and an explicit drop or stop policy. |
| Playback is longer than capture | Fixed nominal FPS in the assembler. | Mux using capture timestamps and a correct time base. |
Lowering everyNthFrame does not reach the target |
The page itself is slow. | Reduce page work or capture dimensions; sampling cannot accelerate rendering. |
| Recording has stale final content | Stop occurs before the consumer drains the last frame. | Coordinate stop, final acknowledgement and worker drain according to your recording policy. |
Performance, reliability and cost notes
- Performance: optimize the largest measured stage. Smaller frames reduce encode, transport, decode and storage work together.
- Reliability: log browser version, CDP method parameters, frame counts, drops, acknowledgement latency, timestamps and final duration so failures are reproducible.
- Backpressure: define what happens when the encoder is slower than capture. Silent unbounded buffering eventually becomes a memory failure.
- Cost: local CDP capture consumes your own CPU, memory, storage and engineering time. A service can move browser operations out of your application, but compare its pricing and output requirements against your workload.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP or PDF. Before capture it accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups and chat widgets. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and responses identify the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients.
See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
You can also set full-page capture, lazy-image loading, CSS selectors for one element, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, click actions, waits, request blocking, headers, cookies, user agent, authorization, timezone, geolocation, transparent backgrounds, resizing, cache TTLs, signed links, asynchronous jobs with signed webhooks and bulk capture of up to 100 URLs per call.
There are 1,000 free screenshots each month with no card. Paid plans start at $5 for 3,000 screenshots, and every feature is available on every plan. Create a free ScreenshotNeo account.
FAQ
Does a higher requested FPS force Chrome to render faster?
No. It can raise the target or cap, but page workload, browser scheduling and host capacity determine achieved FPS.
Should I always use JPEG?
No. JPEG is often efficient for photographic or animated content; PNG may be preferable for lossless UI or transparency. Measure your actual pipeline.
Can I ignore screencast timestamps if my frame count is correct?
No. Correct frame count with incorrect frame durations can produce slow or fast playback.
When is screen recording preferable?
Use Page.startScreenRecording when a direct recording stream with a maximum frame rate and dimensions meets your needs. Use screencast events for custom frame processing.
What is the first change to try?
Instrument acknowledgement latency and compare a lower-resolution JPEG run. Those two tests quickly distinguish protocol backpressure from capture and encoding cost.


