Short answer: your loop cannot guarantee 100 FPS—or even 30 FPS—because page.screenshot(full_page=True) runs synchronously. The next iteration cannot begin until the browser has rendered and returned the image. A 10 ms sleep only schedules the delay after that work. Measure completed screenshots and their timestamps before blaming Pillow, APNG, or a supposed 60-frame limit.
This guide shows how to diagnose the reported 60 frames, reduce capture overhead, preserve accurate timing, and choose a better recording path when you control the chart canvas.
What the current loop actually guarantees
If a screenshot takes 45 ms and your code then sleeps for 10 ms, one iteration takes at least 55 ms—about 18 frames per second. The requested interval is therefore a target, not a measured frame rate. Full-page capture, a 1920×1080 viewport, and device_scale_factor=4 can make each image expensive; treat reducing those settings as a diagnostic, not a proven explanation for the exact 60-frame result.
The question does not include per-capture timings, the buffer length immediately before saving, the APNG’s stored frame count, or browser and machine details. Consequently, no defensible conclusion can say that Playwright, Chromium, APNG, or Pillow imposed a 60-frame cap.
#1 Best Overall
- CRISP CLARITY: This 23.8″ Philips V line monitor delivers crisp Full HD 1920x1080 visuals. Enjoy movies, shows and videos with remarkable detail
- INCREDIBLE CONTRAST: The VA panel produces brighter whites and deeper blacks. You get true-to-life images and more gradients with 16.7 million colors
- THE PERFECT VIEW: The 178/178 degree extra wide viewing angle prevents the shifting of colors when viewed from an offset angle, so you always get consistent colors
- WORK SEAMLESSLY: This sleek monitor is virtually bezel-free on three sides, so the screen looks even bigger for the viewer. This minimalistic design also allows for seamless multi-monitor setups that enhance your workflow and boost productivity
- A BETTER READING EXPERIENCE: For busy office workers, EasyRead mode provides a more paper-like experience for when viewing lengthy documents
First, prove where the frames disappear
- Count successful captures. Print
len(frames)immediately before APNG encoding. The posted loop appends only after a screenshot returns. - Count exceptions separately. A timeout or navigation error should increment an error counter rather than silently looking like a frame-rate problem.
- Log monotonic timestamps. Record times immediately before and after every screenshot. Use
time.perf_counter(), not wall-clock time. - Inspect the saved file. Re-open the APNG and count its frames. If the in-memory list has 300 frames but the file has 60, the loss occurred during encoding or viewing.
- Measure the rate. Divide successful frames by the interval from the first completed capture to the last completed capture. Do not label the result 30 or 100 FPS from the sleep value alone.
import time
frames = []
errors = 0
samples = []
while recording:
started = time.perf_counter()
try:
image_bytes = page.screenshot(full_page=True)
except Exception as exc:
errors += 1
print(f"screenshot error: {exc!r}")
continue
finished = time.perf_counter()
frames.append((finished, image_bytes))
samples.append({
"index": len(frames) - 1,
"started": started,
"finished": finished,
"capture_ms": (finished - started) * 1000,
})
print("successful frames:", len(frames))
print("errors:", errors)
if len(frames) > 1:
elapsed = frames[-1][0] - frames[0][0]
print("measured fps:", (len(frames) - 1) / elapsed)
print("last samples:", samples[-5:])
Use the same timestamp log to compare a small test capture with your production settings. This distinguishes slow rendering from failed calls, an early stop condition, and a viewer that is not displaying every stored frame.
Make Playwright capture as efficiently as possible
Capture only the required pixels
Use a fixed viewport and device_scale_factor=1 while diagnosing. Avoid full_page=True unless the entire document is required; a chart-sized clip or element screenshot moves fewer pixels. Restore higher resolution only after measuring its effect.
context = await browser.new_context(
viewport={"width": 1280, "height": 720},
device_scale_factor=1,
)
page = await context.new_page()
await page.goto(url, wait_until="domcontentloaded")
chart = page.locator("#chart")
box = await chart.bounding_box()
if not box:
raise RuntimeError("chart is not visible")
image_bytes = await page.screenshot(
clip=box,
type="png",
)
A clip avoids unrelated page content. Keep the page open between captures; repeatedly creating contexts or navigating will dominate the timing.
Use an asynchronous loop with a deadline
An asynchronous loop improves coordination but does not make screenshot rendering instantaneous. Schedule the next target time, then record how late the completed capture was. Never pretend a late frame occurred at its ideal time.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #2
- CRISP CLARITY: This 22 inch class (21.5″ viewable) Philips V line monitor delivers crisp Full HD 1920x1080 visuals. Enjoy movies, shows and videos with remarkable detail
- 100HZ FAST REFRESH RATE: 100Hz brings your favorite movies and video games to life. Stream, binge, and play effortlessly
- SMOOTH ACTION WITH ADAPTIVE-SYNC: Adaptive-Sync technology ensures fluid action sequences and rapid response time. Every frame will be rendered smoothly with crystal clarity and without stutter
- INCREDIBLE CONTRAST: The VA panel produces brighter whites and deeper blacks. You get true-to-life images and more gradients with 16.7 million colors
- THE PERFECT VIEW: The 178/178 degree extra wide viewing angle prevents the shifting of colors when viewed from an offset angle, so you always get consistent colors
import asyncio
import time
async def capture_for(page, seconds=6, interval=1/30):
frames = []
deadline = time.perf_counter() + seconds
next_target = time.perf_counter()
while time.perf_counter() < deadline:
started = time.perf_counter()
try:
data = await page.screenshot(type="png")
except Exception as exc:
print("capture failed:", repr(exc))
else:
completed = time.perf_counter()
frames.append((completed, data))
print({
"frame": len(frames) - 1,
"capture_ms": (completed - started) * 1000,
"lateness_ms": (completed - next_target) * 1000,
})
next_target += interval
await asyncio.sleep(max(0, next_target - time.perf_counter()))
return frames
If capture time routinely exceeds 33.3 ms, no loop can produce a sustained 30 completed screenshots per second at that resolution. You must reduce work, accept a lower rate, or change the capture method.
Encode the APNG without hiding failures
Convert returned bytes to RGBA images only after capture has finished, and verify the frame list before writing. Pillow’s duration is metadata for playback; it does not create missing frames or make capture faster.
from io import BytesIO
from PIL import Image
pil_frames = []
for _, data in frames:
with Image.open(BytesIO(data)) as source:
pil_frames.append(source.convert("RGBA"))
if not pil_frames:
raise RuntimeError("no successful screenshots to encode")
# 33 ms is approximately 30 FPS for playback metadata.
pil_frames[0].save(
"recording.png",
format="PNG",
save_all=True,
append_images=pil_frames[1:],
duration=33,
loop=0,
disposal=2,
)
print("encoded APNG frames:", len(pil_frames))
For a variable-rate recording, a single APNG duration cannot represent every measured interval. Either resample frames onto a fixed timeline or accept that playback timing is an approximation. Keep the original timestamps in a sidecar file if timing matters.
Choose the right recording architecture
| Situation | Best starting point | What it produces | Important limitation |
|---|---|---|---|
| Arbitrary rendered webpage | Playwright screenshots | PNG bytes that you can assemble into APNG | Real-time rate is bounded by screenshot latency and memory |
| Canvas you control | Canvas stream plus MediaRecorder | Browser-supported encoded media | Not an APNG writer; MIME support varies by browser |
| Canvas animation requiring exact timing | Fixed-step rendering with WebCodecs or CCapture.js | Encoded chunks or image/video archives | Requires control of the animation; packaging is still required |
Canvas capture with MediaRecorder
If the bar-chart race is drawn on a canvas that your page exposes, capture that surface instead of rasterizing the whole page. Check the desired MIME type first because codec and container availability differ between browsers.
Recommended Free Tools
Rank #3
- Clear visuals. Fluid motion: A 144Hz refresh rate and 1ms MPRT deliver smooth, tear‑free motion across work, gaming, and streaming for clearer, more fluid viewing.
- Eye comfort: TÜV Rheinland 3‑star* certification reduces harmful blue light while preserving stunning color quality without compromise. *TÜV Rheinland 3-star eye comfort certification.
- Wide viewing angle: Get consistent views across a wide 178° /178° viewing angle.
- In-Plane Switching (IPS): See excellent color accuracy and consistency across wide viewing angles with In-plane Switching (IPS) technology.
- Ultra-thin bezels: Maximize your viewing experience with thin bezels.
const canvas = document.querySelector("canvas");
const stream = canvas.captureStream(30);
const mime = "video/webm;codecs=vp9";
if (!MediaRecorder.isTypeSupported(mime)) {
throw new Error("Requested MediaRecorder MIME type is unsupported");
}
const chunks = [];
const recorder = new MediaRecorder(stream, { mimeType: mime });
recorder.ondataavailable = event => {
if (event.data.size) chunks.push(event.data);
};
recorder.start();
// Stop when the animation is complete.
// recorder.stop();
recorder.onstop = () => {
const blob = new Blob(chunks, { type: mime });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "chart.webm";
link.click();
};
This is a media recording, not an APNG. Convert it only if your delivery requirement permits conversion; otherwise capture controlled canvas frames and encode images.
Deterministic fixed-step rendering
For repeatable frame timing, modify the animation so each render accepts an explicit timestamp (for example, 0, 33.333, 66.667 milliseconds) instead of depending on real-time callbacks. CCapture.js documents a virtual clock and fixed-step capture for animations under your control, with video and image-archive outputs; its README notes that custom encoders can support formats such as APNG. It is not a drop-in recorder for an arbitrary third-party page: the animation must cooperate with the virtual clock. See the CCapture.js README.
WebCodecs provides a lower-level VideoEncoder path. Manage encoder backpressure, close every VideoFrame, and call flush() before finalizing. WebCodecs emits encoded chunks; it does not package them as APNG. See MDN’s WebCodecs guide.
Or skip the browser setup
For ordinary webpage snapshots rather than time-based animation capture, ScreenshotNeo makes one HTTP request and returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
See the complete parameter reference in the ScreenshotNeo documentation. A minimal request is:
Rank #4
- CURVED FOR ENHANCED ENGAGEMENT: An immersive viewing experience with a curved monitor that wraps more closely around your field of vision; It creates a wider view, enhancing depth perception and minimizing peripheral distraction
- SMOOTH PERFORMANCE FOR SEAMLESS CONTENT: Stay in the action when playing games, watching videos, or working on creative projects; The 100Hz refresh rate reduces lag and motion blur so you don't miss a thing in fast-paced moments¹
- MORE GAMING POWER: Gain the edge with optimizable game settings; Color and image contrast can be adjusted to see scenes more vividly and spot enemies hiding in the dark; Game Mode adjusts any game to fill the screen so you can view every detail²
- KEEP IT EASY ON THE EYES: Care for your eyes and stay comfortable, even during long sessions; Advanced eye comfort technology certified by TÜV reduces eye strain by minimizing blue light and reducing irritating screen flicker²
- INCREASED VERSATILITY: Connect to more; Plug devices straight into your monitor for increased flexibility, making your computing environment even more convenient
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The same request in Python:
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)
And Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo is not a replacement for deterministic animation capture, but it avoids maintaining a browser worker for static or on-demand shots. It includes full-page and element capture, device presets, custom viewports, retina scale, lazy-image loading, custom CSS and JavaScript, waits, request blocking, cookies, headers, user agents, geolocation, timezone, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Plans include 1,000 free shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshooting checklist
Exactly 60 successful screenshots
Do not infer a cap. Print the buffer length, exception count, first and last completion timestamps, and APNG frame count. Check whether your hotkey handler or recording deadline stops at a fixed duration, and whether a viewer is skipping frames.
Fewer than 30 completed captures per second
Inspect the logged capture duration. Reduce viewport size, set scale factor to 1, capture a chart clip, and remove full-page capture. If latency remains above 33.3 ms, lower the target rate or use a controlled canvas path.
Free tools Windows power users keep installed
One-click scans. No signup required.
Memory grows until the process fails
Raw PNG bytes and decoded RGBA images are both large. Capture to a bounded queue, encode incrementally when your format permits, or shorten recordings. Do not retain duplicate byte and decoded-image copies unnecessarily.
Best Value
- 【INTEGRATED SPEAKERS】Whether you're at work or in the midst of an intense gaming session, our built-in speakers provide rich and seamless audio, all while keeping your desk clutter-free.
- 【EASY ON THE EYES】 Protect your eyes and enhance your comfort with Blue-Light Shift technology. This feature reduces harmful blue light emissions from your screen, helping to alleviate eye strain during long hours of use and promoting healthier viewing habits.
- 【WIDEN YOUR PERSPECTIVE】Our sleek minimal bezel design ensures undivided attention. The nearly bezel-free display seamlessly connects in a dual monitor arrangement, delivering an unobstructed view that lets you focus on more at once, completely distraction-free.
APNG plays too fast or too slowly
The APNG duration is playback metadata, not measured capture timing. Resample to a fixed interval or retain timestamps and choose a player that honors your intended duration.
MediaRecorder throws a MIME error
Call MediaRecorder.isTypeSupported() and select a browser-supported type. A successful recording still requires conversion if APNG is mandatory.
WebCodecs output is incomplete
Limit the encoder queue, close each frame after encoding, await flush(), and package the resulting chunks with a container or image encoder. WebCodecs alone does not create a finished APNG.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePractical decision rule
- Need every pixel of an uncontrolled webpage? Keep Playwright, measure latency, and optimize the captured area.
- Need a normal screenshot or PDF on demand? Use ScreenshotNeo to remove page clutter and avoid maintaining browser automation.
- Own the canvas and need a playable video? Prefer
captureStream()and MediaRecorder after checking MIME support. - Own the animation and need frame-perfect output? Render fixed time steps, then use WebCodecs or a custom image encoder with explicit backpressure and resource cleanup.
Frequently Asked Questions
Can I force Playwright to capture exactly 100 FPS?
No. A synchronous screenshot must finish before the next iteration. You can request a cadence, but only measurements show the achieved rate.
Does setting Pillow duration to 10 ms create 100 FPS footage?
No. It changes APNG playback metadata after capture; it cannot accelerate rendering or recover frames that were never captured.
Can MediaRecorder save APNG directly?
No. MediaRecorder creates browser-supported media, while APNG requires an image sequence encoder or a conversion step.
Is CCapture.js suitable for any website?
No. Its fixed-step virtual clock is intended for animations you can control, not arbitrary third-party pages.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

