In my custom browser Omoikane , I made several changes related to Web API and CSS compatibility.
Looking at these changes, many of them went beyond simply adding APIs visible to JavaScript. It was necessary to convey the page’s state to the host, reflect JavaScript actions in HTTP communication, and preserve the scroll position set by CSS until the input processing is finished.
Cookies were handled separately for JavaScript and HTTP
The first thing we fixed was how cookies are managed.
Previously, the cookie Set-Cookie received from the server was handled by the HTTP layer, while document.cookie referenced a separate JavaScript‑specific Map. As a result, even when a response included a cookie, the page could not read it, and cookies set via JavaScript were not sent in subsequent requests such as fetch or during page navigation.
Since cookies were being stored in two separate locations within the browser, we linked document.cookie and the HTTP request to use the same cookie jar. In addition to storing cookies, we also pass along the target site and whether the request is a top‑level navigation through the communication channel, and we evaluate conditions such as SameSite, Secure, HttpOnly, Domain, and Path. For site comparisons we use schemeful sites and consult the Public Suffix List to prevent public suffixes like co.uk from being accepted as a cookie’s Domain.
With this in place, for example, a cookie set by a script will now be used in HTTP requests for the same page. Conversely, we also test that HttpOnly cookies cannot be read or overwritten by scripts, and that they are not sent to cross‑site communications that do not meet the SameSite criteria.
Simply unifying the cookie storage location was insufficient; we also needed a way to convey, from the request initiator, which page is the origin—whether through redirects, form submissions, iframes, and so on. If the originating site and navigation type are not carried over after a redirect, the SameSite determination can change for subsequent requests even if the initial request was correct.
We performed tests using a local HTTP server, confirming that cookies set by scripts are sent to later requests, that cookies set by the server are visible to the page, and that the sending conditions differ for HttpOnly cookies, cross‑site POSTs, and Lax GETs. Note that some subresource paths—such as stylesheets, images, and module scripts—that have their own HTTP clients remain separate issues.
The changes are PR #862 that linked the cookie jar .
Reflecting the page’s visibility state in the DOM
We also added the Page Visibility API so that the page can be notified when a tab or window becomes hidden. We expose document.visibilityState and document.hidden, and when the visibility state changes we send visibilitychange.
This state cannot be determined solely from the DOM. Since tab switches and window state are managed by the host, the host must notify the internal browser, which then updates the document’s state and events accordingly. State changes are also required when an iframe is destroyed or when the page navigates. We also added a path that specifies Page.setWebLifecycleState to active / frozen via the Chrome DevTools Protocol, handling it together with the host’s visibility.
Instead of discarding a pending requestAnimationFrame simply because the page becomes hidden, we retain it so it can be executed when the page becomes visible again. Although the frozen state and on‑screen visibility are similar, they are not identical, so we combine the separate states to determine the value exposed by the Document.
During this implementation, there was a path that created a child Realm even for iframes without scripts in order to fire their load events. We now create a Realm only when a load handler needs to be executed, avoiding unnecessary initialization. We had to consider both ensuring that events work correctly for compatibility and not imposing extra work on pages that do not use events.
We also made the options for addEventListener common across all DOM EventTargets. If signal is aborted, we remove the listener and its associated detach handler, and within the passive listener, preventDefault() does not cancel the event. options: null is also treated as a default value rather than an exception.
Previously, the root wheel listener was not treated as passive, allowing page‑side listeners to sometimes block the default scrolling. We unified the handling of options that were being interpreted separately per event target, so that Nodes, ordinary EventTargets, XHR, and their derived targets all use the same detachment rules. The WPT once, passive, and signal tests are also run against a fixed revision.
The Page Visibility changes are PR #864 for the Page Visibility API , and the listener‑option changes are PR #860 for event listeners .
Determine the scroll position up to the final stage of the input pipeline
With CSS Scroll Snap, even when scroll-snap-type and scroll-snap-align are specified, the scroll’s end point did not align with the snap positions. Simply calculating the styles was insufficient; we needed to actually select the location where the scroll stops.
We added logic to compute the snapport of the scrollable area and each element’s snap area, then choose the end point based on mandatory, proximity, scroll-padding, scroll-margin, and so on. Candidate positions are evaluated along both physical and logical axes. The same decision is applied not only to scrolls driven by scripts but also to wheel events, smooth scrolling, scrollIntoView(), and positions after relayout. We also added regression tests for RTL layouts, vertical text, nested scrollable regions, and cases where the snap area exceeds its container.
Relatedly, we implemented smooth scrolling using scroll-behavior and ScrollOptions.behavior, as well as scroll‑chain control via overscroll-behavior. Smooth scrolling is interpolated on the scheduler, handling interruptions from intermediate input, cancellation during navigation, and event ordering. When an inner scrollable region reaches its edge, we handle per‑axis whether to pass remaining wheel or touch input to the outer region, and contain and none also distinguish the host’s overscroll behavior.
When comparing against a fixed fixture, the expected stop position changes depending on whether snapping is enabled, and the pixel differences in rendering comparisons with Firefox for the new fixtures became zero. However, some scroll tests on the root/window still exhibit known failures that appear to be caused by viewport overflow handling separate from snapping. The changes are PR #866 for CSS Scroll Snap .
Also updated fine‑grained aspects of HTTP and CSS
In the HTTP layer, we switched to the WHATWG URL parser for URL parsing, reexamined request‑header validation, the header policy for redirects, and the framing of HTTP/1 responses. The previous custom parser could mishandle IPv6 authority, userinfo, and relative‑URL resolution, and it also risked propagating credentials to the redirect target. We use the same parser for both JavaScript’s URL and network requests, validate request headers before they are written to the wire, and decide how to handle sensitive headers in redirects in a single place.
For HTTP/1, we skip informational responses, treat statuses that include neither a HEAD nor a body, and also inspect the order of transfer codings, chunk sizes and termination, and trailers; connections with ambiguous framing are not reused. We set a default 64 MiB upper limit on response buffering for both HTTP/1 and HTTP/2, which can be changed via OMOIKANE_MAX_HTTP_BODY_BYTES. This change prevents cases where a very large response is allocated all at once or where broken framing is mistakenly interpreted as the next response.
PR #859 that hardened HTTP processing
In the CSS @property and CSS.registerProperty(), we validate duplicated registered names, syntax, initial values, and inheritance, and connect the computed values of typed custom properties to transitions and animations. In @counter-style, we register systems such as cyclic and extends along with their symbols, prefixes, and suffixes, and resolve styles with the same name according to origin, cascade layer, and source order.
For printing in @page, we pass the print media independently of the screen‑style resolution, selecting selectors and declarations based on origin, layer, specificity, and source order. We handle paper size, margins, named pages, and basic page breaks, but margin boxes and full per‑page relayout are beyond the scope of this work.
In multicol, block child elements could not be split, so the entire element was placed in a single column. We retain the source/target geometry for each fragment and share it for column balancing, getClientRects(), painting, hit testing, and scroll overflow. box-decoration-break: slice | clone is also handled, ensuring that semi‑transparent backgrounds do not become overly dark by overlapping across fragments; cloned decorations are rendered only once. In the fixed Firefox fixture, the geometry for the new three cases matched the RGBA pixel values.
PR #834 adding @page support for printing
,
PR #824 for multicol fragmentation
Revised the DOM hot path to meet Acid3’s time limits
As compatibility features increase, even pages that do not use those features can experience slower DOM operations. Because Acid3 test 26 and attribute_records_gc approached the five‑second limit, we re‑measured DOM attribute accesses and mutation handling.
For example, NamedNodeMap.length and item() used to clone the entire attribute record before converting it, even though only the requested number of attributes—or just a single one—needed to be returned. We changed this to read only the necessary values. Additionally, when a custom element definition is absent we skip dedicated subtree bookkeeping, and when no MutationObserver is present we avoid sibling searches, thereby removing unused hooks from the hot path.
When compared within the same x86 runner, there was a run in which Acid3 test 26’s Direct mode improved from 4.08 seconds to 3.46 seconds, Faithful mode from 3.71 seconds to 3.19 seconds, and another run where attribute_records_gc dropped from 4.35 seconds to 1.83 seconds. Because runner performance can vary, a single figure cannot be generalized, but within the same run the improvements are evident, and both modes continue to score a perfect 100/100 on Acid3.
PR #835 improving DOM hot‑path performance
Compatibility is determined by state propagation
In this round of work, we often had to align states that reside in separate layers—for example, cookies involve JavaScript and HTTP, page visibility involves the host and the Document, and scrolling involves CSS calculations versus actual input and rendering. WPT runs each modified area against a fixed revision to verify that no regressions have occurred. In the full test suite, the recent page‑visibility work succeeded in 2,886 tests, scroll‑snap succeeded in 273 tests, and only 25 known root/window‑related subtests are currently being tracked in separate issues.
Adding support for API names or CSS properties can be relatively quick. However, browser compatibility is ultimately determined by ensuring that the state observed by a page remains consistent all the way through communication and rendering. We will continue to expand specification coverage while also using fixed Web Platform Tests and browser comparisons to verify that behavior stays coherent.