Patrick Meenan

Welcome!

This is my personal blog and mostly contains my random thoughts about web performance, development, browsers or whatever else I might be thinking about at the time.

Latest Posts

New field browser performance and profiling tooling - rumcap

Ever since browser vendors started exposing advanced performance telemetry APIs directly to the page, like the Performance Timeline, Long Animation Frames (LoAF), and the JS Self-Profiling API, I’ve been waiting for tooling that could capture, store, and visualize all of this data in the wild. We can gather incredibly detailed information about what is happening on a user’s machine, but adoption of the profiling side of things has been pretty light from what I can tell (and the lack of tooling doesn’t help).

When I introduced Waterfall Tools a few months ago, the goal was to build a client-based canvas rendering engine for synthetic test waterfalls. But it also laid the groundwork for a field-data viewer. We just needed a way to package, compress, and feed that data into it.

Today, I’m excited to introduce rumcap: a file format and helper library designed specifically to collect, compress, and visualize RUM performance data.

Half the Size of Gzipped JSON

Besides just be a well-defined way to faormat the data for consumption, the main benefit of rumcap is its compression efficiency.

When you capture raw performance timelines, resource entries, call stacks, and JS profile samples, the raw data is highly repetitive. You have the same domains, similar resource paths, repeated function names, and overlapping execution call stacks. If you just grab these events as JSON and gzip them, you still end up with relatively large payloads.

rumcap serializes the data into an extensible binary format that is optimized to compress well (and gzip compression is applied as part of the packaging).

The result? A .rumcap binary file is typically half the size of the equivalent raw events gzipped. By profiling standards, the files are tiny, making rich telemetry viable for real-user monitoring (in the neighborhood of 10 KB for a typical page view with full request data and JS self-profiling enabled).

Visualization In Waterfall Tools

To support this new format, waterfall-tools has been updated with native rumcap support, exposing the data in the regular waterfall view as well as the embedded Chrome DevTools viewer and Perfetto trace viewer (with whatever event data was included in the capture restructured into the correct format for each):

  1. Waterfall View: A classic network waterfall timeline showing all resources, including visual markings for key performance metrics (FCP, LCP) and Long Animation Frames (LoAF).

Waterfall View
The Waterfall view rendering resource requests in the classic performance waterfall.

  1. DevTools View: Chrome’s DevTools performance panel with data for the requests, page-level timings, call stacks, user-timing events, and more.

DevTools View
A Chrome DevTools performance panel with full call stacks and request data.

  1. Perfetto View: An embedded Perfetto trace viewer for slicing and dicing the JS profile samples alongside main thread activities (with the request details and timings preserved as well).

Perfetto View
An embedded Perfetto trace view of the JS profile, request data and other timing events.

If your profile includes JS self-profiling data, you can view the call stack flame charts side-by-side with resource requests and LoAF blocks to instantly identify what script blocked the thread. Additionally, it supports User Timing marks and measures, as well as custom stacked event durations so you can map your own trace instrumentation into the timeline.

What rumcap is NOT (and What it is)

To be clear, rumcap is not a full end-to-end RUM stack.

It does not provide:

  • A full analytics/beaconing library (which projects like Boomerang are already great at).
  • A collection backend or aggregation infrastructure.
  • A visualization dashboard.
  • Any aggregation across events.
  • The logic to decide when to trigger profiling (since JS self-profiling does have runtime overhead, you definitely shouldn’t capture a full trace on every single page view).

rumcap provides a well-defined format for storing the data (efficiently), an encoder/decoder library, and the visualization tools. The goal is to make it easy for existing RUM providers and in-house performance infrastructure to start collecting and displaying developer-friendly trace views.

In-Page Example

Integrating rumcap to capture events and profile JS on your page is designed to be as simple as possible, providing a sink that you can pipe PerformanceObserver and Profiler events directly into.

<script type="module">
  import { Encoder, entrySink, environmentSnapshot } from '/js/rumcap.js';

  const encoder = new Encoder();
  encoder.setEnvironment(environmentSnapshot());

  // Connect PerformanceObservers directly to the encoder sink
  const sink = entrySink(encoder);
  const entryTypes = [
    'navigation', 'resource', 'paint', 'largest-contentful-paint', 
    'layout-shift', 'event', 'first-input', 'longtask', 
    'long-animation-frame', 'element', 'mark', 'measure'
  ];
  for (const type of entryTypes) {
    try { new PerformanceObserver(sink).observe({ type, buffered: true }); } catch (e) {}
  }

  // Start JS Self-Profiling if supported
  let profiler;
  if (window.Profiler) {
    profiler = new window.Profiler({ sampleInterval: 10, maxBufferSize: 30000 });
  }

  window.addEventListener('load', () => {
    setTimeout(async () => {
      if (profiler) {
        try {
          const profile = await profiler.stop();
          encoder.addProfilerChunk(profile, profiler.sampleInterval);
        } catch (e) {}
      }

      const bytes = await encoder.finish();
      // navigator.sendBeacon('/rum', bytes);
      console.log('Captured rumcap size (bytes):', bytes.byteLength);
    }, 1000);
  });
</script>

Demos

Here are a few sample captures from production pages in the Waterfall Tools viewer (all captured at 6x CPU throttling to show more interesting events):

Open Source & Libraries

All parts of both projects are free, open-source, with Apache 2.0 licenses and available on GitHub and npm:

Feel free to play around with the tools, try it out, and share your feedback on GitHub (issues and PR’s happily accepted)!

Introducing Waterfall Tools

I’ve been wanting to build a 100% client-based waterfall tool for a long time. Something with a much more modern rendering engine and UI than WebPageTest’s server-side php-based image waterfalls. It’s a fairly big project though and I never had the time to invest into it but that all changed with the advance of the AI-assisted development tools (I refuse to use the term “vibe coding” given the amount of actual engineering that went into it).

Today, I bring you Waterfall Tools!

Waterfall
Waterfall

What is it?

Waterfall Tools is a JS library that provides the necessary logic for ingesting a HUGE variety of data formats, extracts the waterfall page and request data, and provides interfaces for rendering it using canvas into a container that you provide. It provides hooks for interacting with the waterfall (hover, click and request data) so you can build a full viewer.

It ALSO provides a full viewer that can be embedded in an iFrame or loaded directly with query params that allow you to control the behavior and provide a source URL for the data to be rendered.

As a stand-alone viewer, it can work completely client-side and you can drag and drop an appropriate file onto the viewer and it will provide a rich view of the details of the request data, a WebPageTest-style waterfall and integration with other tools like Perfetto and Chrome’s netlog viewer, all within the client UI.

It can run completely offline with service worker support.

If the source data is loaded from a URL, the resulting waterfall view is sharable, down to the specific view you are looking at.

It supports multiple pages within a single data source (e.g. a WebPageTest agent run with multiple pages, or a HAR file with multiple pages).

The canvas-rendered waterfalls can still be copy/pasted or directly saved as images so they can still easily be dropped into documents and presentations.

Page Selection screen
Page Selection screen

What formats does it support?

Waterfall Tools supports every format I could think of to implement and supports adding more as needed. The current list includes:

  • WebPageTest agent (wptagent) raw test results (sample)

  • WebPageTest JSON (sample)

  • WebPageTest HAR files (including those from the HTTP Archive) (sample)

  • Chrome Netlog captures (sample)

  • Chrome Trace capture (in both perfetto protobuf and JSON formats) (sample)

  • Chrome HAR files (sample)

  • Firefox HAR files (sample)

  • Raw tcpdump captures (with keylog files for TLS decryption) (sample)

I’m particularly excited about the tcpdump support. It handles:

  • Decoding the capture files
  • Building the TCP and UDP streams
  • Decrypting TLS traffic using the keylog file (including QUIC TLS1.3)
  • Extracting HTTP/1.x, HTTP/2 and HTTP/3 requests and responses (including HPACK and QPACK decoding)
  • Decoding DNS traffic
  • Decoding DNS over HTTPS (DoH) traffic
  • Decoding DNS over TLS (DoT) traffic
  • Extracting response bodies
  • Decompressing gzip, zstd and br content-encodings

All in 100% vanilla javascript leveraging browser APIs where possible.

Request Details

Clicking on any request in the waterfall will open (or focus) a closable tab with the request details that you’re used to seeing in WebPageTest’s pop-up dialogs.

Request details
Request details (image)

Including syntax-highlighted response bodies for text-based content types.

Request details
Request details (javascript)

Embedded Viewers

Beyond the directly-owned UI for rendering the waterfalls and request data, the viewer also integrates with other tools for relevant formats depending on what the test data includes.

For example, if the data source is a Chrome netlog or the test data includes a captured netlog, the viewer provides a “Netlog” tab that embeds the Chrome Netlog Viewer to provide a rich view of the network traffic.

Embedded Netlog viewer
Embedded Netlog viewer

Similarly, if the data source is a Chrome trace or the test data includes a captured trace, the viewer provides a “Trace” tab that embeds the Perfetto UI to provide a rich view of the trace data.

Embedded Perfetto viewer
Embedded Perfetto viewer

If the test data includes lighthouse results, the viewer embeds the lighthouse result in a “Lighthouse” tab.

Embedded Lighthouse viewer
Embedded Lighthouse viewer

Waterfall customization

The waterfall rendering is highly configurable and you can control all of the things you are used to being able to control in WebPageTest’s waterfall viewer and then some.

Waterfall Options
Waterfall Options

I’m particularly happy that we can now, FINALLY, adjust the start time of the waterfall to zoom in on a section in the middle of a waterfall. This is something that has been requested for years for WebPageTest and I’m glad we can provide it here.

The connection view and data charts below the waterfall are also supported including request-specific details as you hover over or click on the individual chunks in the connection view.

Connection View
Connection View

How big is it?

I made the conscious decision to focus on a vanilla javascript project that requires “modern” browsers and went with a full javascript application including a lot of the UI. A lot of the core functionality requires it anyway and it wouldn’t make sense to try to jump through hoops to use native browser elements and css for things like the waterfall rendering.

The core library is split into 3 pieces (all sizes are reported in compressed wire-sizes):

  • 40 kB : The core waterfall tools JS (which includes support for parsing all of the formats except tcpdump and the canvas rendering engine)
  • 17 kB : The tcpdump parser including all of the decryption and decoding logic and everything except for JS-based Brotli decompression.
  • 59 kB : Javascript-based Brotli decompression (for browsers that don’t support DecompressionStream('brotli'))

The tcpdump and Brotli support are loaded on-demand as-needed (and hopefully the Brotli support can go away entirely over time as browsers add native DecompressionStream support).

The Viewer UI has a few pieces between the HTML, CSS, JS and logo image. All-in, that comes in at around 25 kB for the viewer UI.

The viewer bundles the Chrome Netlog viewer if you want to be able to view netlogs in the native viewer and it is a bit of a pig, relatively speaking at 170 kB.

For a core Waterfall client supporting everything except for tcpdump import and without built-in netlog viewing, that comes in at around 65 kB.

I’m sure it will grow over time as more features are added but I’m very happy with that for the level of functionality it provides.

Free and Open Source

The project is as unburdened with licensing as possible. The code and all of it’s dependencies are under Apache 2, MIT, BSD or equivalent licenses, allowing you to do anything you want with it (including commercially).

You can find the repository here: https://github.com/pmeenan/waterfall-tools

Please file issues for anything you see that is broken or that you’d like added or changed and contributions are welcome.

The project is designed to be developed with the help of AI coding assistants with a running set of agent instructions in AGENTS.md (and referenced from CLAUDE.md) so they should be picked up automatically.

The viewer is available at https://waterfall-tools.com and you can play with it right now with the samples linked above or by dragging and dropping your own files onto the page.

What’s Next?

There are still a lot of features I’d like to add and I’m sure there are a lot of edge-case bugs that still need to be fixed (what is there now is the result of about a week’s worth of weekends and evenings).

Some of the things on my TODO list include:

  • Filmstrip view (this is a big one)
  • “All images” view that shows all of the images that were loaded and any optimization opportunities
  • Console log in the Summary tab
  • More metrics extracted from the tcpdump, netlog and Chrome trace files

The results viewing is also just a lego piece in a full testing pipeline. I’d like to see if I can connect the viewer directly to local test tooling like wptagent’s CLI, Puppeteer, Playwright, Crossbench, etc. so that you can run tests locally and view the results in the viewer without having to upload files to a server.

It could also be interesting to hook up to a simple test queuing system that runs tests on remote infrastructure, using something like the old WebPageTest API to submit jobs and get results back.

If you chain that with persistent storage somewhere, you basically have a full synthetic testing pipeline with swappable pieces.

Shipping jQuery and React Frameworks with Chrome

Should we ship jQuery, React and other popular frameworks with browsers so sites don’t have to re-download the same frameworks over and over?

Some background

For years, web performance advocates have casually suggested that browsers should “just ship jQuery” or other popular frameworks to avoid the need for every site to force users to re-download identical library code (there’s a recent WHATWG discussion on it here).

However, this concept has historically faced several fundamental hurdles.

First, there is a massive variety of framework versions in active use across the web, making it almost impossible to select a single “canonical” version.

Second, sites must be able to react quickly to security vulnerabilities, and being “locked” to a browser-shipped version could seriously hinder necessary updates.

Finally, these frameworks are served from a wide array of domains and are frequently bundled with site-specific code, which completely breaks simple URL-based caching.

Proposal: A Web-Wide Compression Dictionary

Compression dictionary transport brings an interesting possible solution to this problem. Instead of shipping raw library binaries, we could ship a versioned compression dictionary (e.g., a “2026 web” dictionary) that includes common frameworks like React and jQuery. This is basically the modern alternative to the old “Built-In Web Libraries” approach.

Unlike the whole bundling approach—which struggled with the sheer variety of library versions and the risk of making certain versions “sticky” and slowing down security updates—a compression dictionary provides a wonderfully transparent mechanism. It allows servers to compress their unique resource bundles against the shared dictionary, gaining cross-site sharing benefits without requiring developers to change their HTML or worrying about being locked into a specific binary version. The dictionary natively supports versioning and avoids the privacy risks or other concerns associated with traditional shared library caching schemes.

Since libraries tend to change pretty incrementally over time, a single version of jQuery, React, or other commonly used code can actually compress other versions of the same library really well, eliminating the need to match a site’s specific version.

Even better, the proposal leverages the existing Compression Dictionary Transport mechanism and the Available-Dictionary request header for seamless backward compatibility and easy deployment. The browser would just advertise the web-wide dictionary as being available when a better, content-specific dictionary is not.

Methodology: Building the Dictionary

So how do we build it? The 50MB dictionary was constructed by analyzing massive amounts of public web data pulled as part of the HTTP Archive crawl. For this run, the crawl was updated to parse all of the Javascript it encountered, extract each top-level comment and function block, and store them in the crawls_staging.script_chunks table along with the hash of the payload and the URL it was served from.

(The code for generating and testing the dictionary is up on Github in the web-dictionary project).

We counted the unique occurrences of those hashes across different URLs and pulled the script chunks that were seen on at least 10,000 different URLs. That yielded around 10,300 highly pervasive script or comment blocks. A deduplication pass was then used to ensure that similar functions—such as those across different versions of the same library—were compressed against each other. This was purely to minimize the dictionary size while maximizing utility. The resulting dictionary is around 50MB and works with both Brotli and ZStandard.

The tested dictionary contains a lot of the typical boilerplate copyright blocks as well as the frameworks you’d normally expect to see out there (jQuery, jQueryUI, React, Preact, Angular, etc.), plus a lot of underlying code that is widely reused.

Methodology: Testing the Dictionary

To actually test the effectiveness of this beast, I pulled the list of script and HTML requests that were loaded by the top 100,000 pages from the March HTTP Archive crawl. That resulted in ~3 million unique URLs.

I then fetched the URLs independently to keep BigQuery costs in check (even though the HTTP Archive has the original bodies) and re-compressed them twice: once with Brotli level 11, and once with Brotli level 11 plus the 50MB dictionary. The original encoded size as-served from the origin was also logged, and then the relative sizes were compared for analysis.

Experimental Results

I stopped the processing at ~400k URLs (70% scripts, 25% HTML) because the data converged really quickly and wasn’t changing as more URLs were processed.

Here’s how the savings looked:

Bar chart of framework dictionary compression savings
Framework Dictionary Compression Savings

Script Metrics

  • Brotli 11 Savings over Original: saved 16% (11 KB).
  • Brotli 11 + Dict Savings over Original: saved 29% (15 KB).
  • Brotli 11 + Dict Savings over Brotli 11: saved 15% (4 KB).

HTML Metrics

  • Brotli 11 Savings over Original: saved 35% (8 KB).
  • Brotli 11 + Dict Savings over Original: saved 55% (9.5 KB).
  • Brotli 11 + Dict Savings over Brotli 11: saved 27% (1.7 KB).

Conclusion

While the inclusion of a 50MB framework dictionary does offer some really great compression benefits—particularly for HTML and certain classes of scripts—the overall conclusion is that it’s just not yet worth the effort right now.

Managing a 50MB static dictionary on every single client device and growing adoption of server-side compression with the same dictionary is a fairly long and drawn-out process.

Given that just using Brotli 11 compression alone already provides significant savings over what most websites are currently serving (and tuning their compression is exactly what we’d have to do for the dictionary support anyway), the most effective path forward is to encourage broader adoption of Brotli 11 well before we start introducing the overhead of a browser-shipped web-wide dictionary.

View All Posts in Archive
Enlarged view