Update on what happened in WebKit in the week from October 12 to October 19.
Another busy week for the team! This week we had a variety of fixes, including the regular batch on the Layer-Based SVG Engine, a new pair of properties for WPE WebKit, and a new ApplicationManifest API which improves support for progressive web applications.
Add new ApplicationManifest API. This allows looking up the webmanifest for Progressive Web Applications.
Fixed the repaint rects of SVG text using vector-effect: non-scaling-stroke, first by taking the scale into account in the Layer-Based SVG Engine (LBSE), which used to repaint a larger area than it paints for scaled-up text. A follow-up made the decision per text run, since vector-effect is not inherited and a <tspan> inside a non-scaling <text> paints a regular scaling stroke, and unified the computation so that the legacy SVG engine and LBSE now report identical repaint rects.
Fixed the composited bounds of layers below pixel-moving CSS filter effects such as blur() and drop-shadow(), whose backing store previously missed the pixels the filter samples outside the clip. The clips above a layer are now expanded by the filter outsets of the layer and its ancestors, and a change to those outsets updates the composited bounds of all descendants. This change fixes the cross-platform part, and the CoreAnimation based ports, as well as GTK/WPE.
Fixed repaints when an SVG element switches to a wider clip-path, mask or filter in the Layer-Based SVG Engine (LBSE), which previously only repainted the area it covered before the change and left the newly revealed area stale. Style changes that alter the visual overflow now trigger a repaint of the new bounds as well.
Fixed the repaint region of elements clipped by a <clipPath> with a transform attribute in the Layer-Based SVG Engine (LBSE), where changing that transform repainted the wrong area.
Fixed mix-blend-mode on a document-root <svg> element in the Layer-Based SVG Engine (LBSE), which wrongly blended the page background along with the SVG contents. The blend mode is now applied to the SVG contents only, matching the legacy SVG engine.
Fixed elements staying clipped on screen after their <clipPath> was removed in the Layer-Based SVG Engine (LBSE), because the clipped elements were never repainted. Destroying an SVG resource now notifies its clients, which repaint the area the resource used to cover.
Fixed hit testing of children inside transformed layered SVG groups in the Layer-Based SVG Engine (LBSE), where points outside a shape could hit it and transformed children could be missed entirely.
Stopped recomputing the transform twice on nearly every layout of an SVG container without a layer in the Layer-Based SVG Engine (LBSE). The transform reference box is now remembered for such renderers when transform-box is view-box (the default), so the post-layout update only runs when the box actually moved.
Fixed repaints of SVG groups and their content in the Layer-Based SVG Engine (LBSE), which previously left stale content on screen when something inside a <g> moved or when the <g> itself changed (a new transform / changed clip-path etc.). Images, text and shapes now compute their final position during their own layout, and groups with a layer remember their transform so they repaint both the old and new area on change, which brings text repaints in transformed groups in line with the legacy SVG engine.
Fixed painting of descendants with visibility: visible inside hidden <text> and <foreignObject> elements in the Layer-Based SVG Engine (LBSE), which skipped the whole subtree, unlike the legacy SVG engine. Hidden <g> and <text> containers now also run the mask and clipping mask phases, so such descendants are masked and clipped as well.
On WPE expose WindowProperties:geometry and WindowProperties:has-custom-geometry like GTK.
That’s all for this week!
Apple and WebKit have shipped protections against web tracking for a long time. We put privacy protections in place for cookies in the very first version of Safari, we invented storage and cache partitioning and shipped it ten years before other major browsers, and we’re actively fighting device fingerprinting.
We do this because users deserve to not be tracked, not across websites and not covertly through fingerprinting, unless they explicitly opt in to it. See WebKit’s Tracking Prevention Policy for a comprehensive overview.
We fully blocked cross-site cookies six years ago. That was a huge step forward for privacy on the web.
However, the tracking industry has not sat idle since.
A “post-cookie” tracking method emerged, called identity resolution. In this scheme, third parties (i.e., not the website owner) invisibly collect your email address when you log in or sign up for a newsletter. They send the email address to their servers, create an opaque tracking ID permanently linked to your email address, and send that tracking ID back to the webpage. The tracking ID is stored on the website and can from thereon be used for arbitrary purposes linked to your email address, such as building a profile of your browsing or targeting ads at you across websites. Logging out or unsubscribing from that newsletter does not remove the tracking ID or your email address stored with the third party.

Over time, these third parties collect even more data as you log in using your email address or fill in forms with personal data. They use this to create a data profile of you, known as an identity graph. Here’s an illustrative example of what such a graph may look like:

Email addresses almost always constitute Personally Identifiable Information, or PII. Another example is your phone number. Such information not only allows for cross-site tracking, but also cross-device tracking. For the majority of users, their email address serves as a globally unique ID that they keep for years, even decades. When users provide information for one purpose, it breaks their expectation to use it for another unstated purpose. For example, a user who logs in or signs up for a newsletter is not expecting to have their information sent to a third party for other, potentially long-term, intents.
First party personalization is not tracking, and is not what our technologies seek to prevent. But even if identity graphs are only scoped to data from a single company, i.e. a first party, the central identity resolver is a third party and it holds cross-site, cross-device, and cross-party data about the user and can perform matching and targeting functions in the capacity of a clearinghouse. That is a tracking capability that each first party does not have on their own.

Some companies serve as third-party infrastructure to facilitate identity resolution. They are present but invisible on many webpages where you enter your email address or use your email address for login purposes. They are not themselves login providers or newsletter services, they’re just there to track you.
In our iOS, iPadOS, macOS, and visionOS 27 releases, Safari and WebKit block network access to identity resolution infrastructure to prevent cross-site tracking. This stops them from sending your email address to their servers and all the downstream effects of that. The blocking is done on the domain level, meaning they can’t just change the structure of the network request to work around the protection.

As an iCloud+ subscriber, you are offered the service Hide My Email which enables you to submit unique, randomized email addresses to every new website while still getting their messages in your inbox. Unique, random email addresses provide additional protection against email-based tracking.
It’s not enough to block cross-site cookies to prevent cross-site tracking today because the tracking industry has not stopped trying to track users on the web. They have ushered into what they call the post cookie era and in our latest releases we have put strong protection in place. We will continue to prevent covert and cross-site tracking by default, while maintaining the ability for websites to provide first-party personalization to their users.
Update on what happened in WebKit in the week from October 5 to October 12.
A CSS invalidation bugfix, moving steps for optgroup and hr elements, and a handful of fixes to the Layer-Based SVG Engine compose this week's bag of updates, changing to a calmer pace compared to the packed weeks we've had recently!
Fixed various CSS selector invalidation when an element is moved.
Add moving steps for optgroup and hr elements.
Fixed buffered-rendering="static" for SVG <image> elements in the Layer-Based SVG Engine (LBSE), where the image used to be drawn at the wrong position inside its offscreen buffer and without its own transform. The image is now painted into the buffer relative to its own position, and the buffer is drawn back with the current transformation, which matches the behaviour of the legacy SVG engine.
Added support for pointer-events: bounding-box on SVG text in the Layer-Based SVG Engine (LBSE), so clicks that miss every glyph now fall back to hitting the text's bounding box (see pointer-events). It also fixed hit testing of transformed children in SVG containers, which were being tested a second time without their transforms.
Fixed text not following its path in the Layer-Based SVG Engine (LBSE) when a <textPath> points to a path that doesn't exist yet. The text is now laid out along the path as soon as that path appears in the document, which also covers removing and re-adding the path or changing href to another path.
That’s all for this week!
Safari Technology Preview Release 254 is now available for download for macOS Golden Gate and macOS Tahoe. If you already have Safari Technology Preview installed, you can update it in System Settings under General → Software Update.
This release includes WebKit changes between: 321068@main…322634@main.
pointer-events: none. (322472@main) (188003870)menuitem, menuitemcheckbox, and menuitemradio as “selected”. (322397@main) (188185386)opacity to 0, including scroll-driven animations, could leave elements fully visible instead of transparent. (322355@main) (187682187)size, min-size, and max-size shorthand properties. (321710@main) (187495014)border-*-radius side shorthands, such as border-top-radius, which set the radii of both corners on one side of a box at once. (322537@main) (187605885)interpolate-size property, which allows animations and transitions to and from intrinsic sizing keywords such as auto, min-content, and fit-content. (321370@main) (187701518)symbols() function for defining anonymous counter styles. (321624@main) (188048446)link-parameters property and param(), for passing values into linked SVG images. (321822@main) (188217737)calc-size() function, which allows calculations on intrinsic sizes such as auto, fit-content, and max-content. (322574@main) (188996389)order values. (321853@main) (173962596)width: 0 column being placed off-screen. (322376@main) (183452074)outline-offset on elements that use corner-shape. (321259@main) (186820849)line-clamp and -webkit-line-clamp to share the same underlying values and cascade correctly against each other, with -webkit-line-clamp now treated as a shorthand. (322292@main) (187100061)sibling-index() and sibling-count() returning 1 for children of a shadow root. (321182@main) (187201634)line-clamp, max-lines, and block-ellipsis to match the latest specification syntax, allowing max-lines and line-clamp to combine auto with an integer and block-ellipsis to accept no-ellipsis, ellipsis, or a string. (321832@main) (187371224)min-content inline size to give both the min-content and max-content contributions of an element that size. (321278@main) (187601903)grid-template-areas sometimes being empty. (321401@main) (187753954)fit-content() grid tracks growing beyond their limit when distributing extra space. (321720@main) (187757242)clip-path with a <geometry-box> value ignoring corner-shape and clipping to rounded corners instead. (321557@main) (187891292)fit-content. (322557@main) (187932371)width ignoring max-width: max-content when contributing its intrinsic size. (322298@main) (188008779)contain: inline-size, container-type: inline-size, or content-visibility: hidden so it comes from contain-intrinsic-size rather than the grid items. (321612@main) (188029152)table-layout: fixed being ignored when a table’s width is intrinsic, min-intrinsic, or a calc-size() value. (322409@main) (188201473)none channel values that are referenced directly, so oklab(from oklab(none none none) l a b) resolves to oklab(none none none). (321724@main) (188215354)none components serialize using color(srgb ...), and hsl() and hwb() colors serialize their saturation, lightness, whiteness, and blackness components as percentages. (321758@main) (188254602)none) components are preserved instead of becoming 0 when the origin color is converted to a different color space. (321802@main) (188300348)alpha() colors to serialize in the most precise form using color() syntax, matching color-mix() and other relative color expressions. (321829@main) (188323511)alpha() color functions to require an alpha parameter, so values like alpha(from red) are now invalid, following a CSS Working Group resolution. (321852@main) (188339902)ActiveBorder, Menu, Scrollbar, ThreeDFace, and Window, to compute to their CSS Color 4 equivalents on macOS. (322193@main) (188365953)border-image-repeat: round to round the tile count to the nearest whole number instead of rounding it down. (321996@main) (188369219)aspect-ratio inside a column flex container did not shrink when the container got narrower. (321951@main) (188393652)line-clamp to make room for the block ellipsis during line breaking, moving content that doesn’t fit next to the ellipsis to the next line, including nowrap content. (322495@main) (188393991)line-clamp so that when the block ellipsis displaces all content on the clamped line, only the ellipsis remains and the line is not made taller by inline boxes. (322500@main) (188440163)aspect-ratio and a percentage height did not shrink when the grid got narrower. (322176@main) (188444964)hsl() and hwb(). (321984@main) (188471496)line-clamp adding a block ellipsis to the last line when all of the content fits within the line limit. (322510@main) (188500712)line-clamp to count the lines of nested blocks towards the maximum number of lines. (322521@main) (188535368)line-clamp to remove hanging trailing whitespace before the block ellipsis. (322531@main) (188551752)line-clamp was ignored on floats and absolutely positioned boxes. (322551@main) (188552622)line-clamp container. (322578@main) (188552871)display: contents element inherited the wrong style after its parent’s style changed. (322092@main) (188591349)line-clamp incorrectly clamping lines on a multi-column container. (322584@main) (188595965)line-clamp container showed the lines past the clamp point. (322600@main) (188596942)aspect-ratio being ignored on absolutely positioned boxes with height: fit-content, min-content, or max-content. (322178@main) (188597811)line-clamp: auto to clamp at the container’s block size. (322603@main) (188621599)shape-image-threshold did not update the shape when the shape image was a gradient. (322232@main) (188772999)::first-line styles in certain cases. (322319@main) (188830233)fit-content() grid tracks with a percentage argument being stretched when the grid’s size is indefinite. (322421@main) (188848301)overflow-wrap: anywhere keeping extra characters on a line, or breaking inside a surrogate pair, next to punctuation that is prohibited at the start of a line. (322554@main) (188884467)position-visibility to be anchor-visible. (322565@main) (188939499)rect() using the wrong winding direction when given a negative width or height. (322101@main) (188036092)TextMetrics emHeightAscent and emHeightDescent to add up to the font’s em size, and updated the top, hanging, middle, ideographic, and bottom baselines to match when drawing text. (322077@main) (188368945)drawImage() and createPattern(), createImageBitmap(), WebGL, and VideoFrame to follow CSS object size negotiation. (322003@main) (188524860)darker value of globalCompositeOperation. (321811@main) (188035440)position: fixed element so that the page autoscrolls when the pointer moves past the edge of the window. (322310@main) (188330363)type attribute on the resulting <ol>. (322170@main) (188445470)::picker-icon on <select> elements to draw the dropdown arrow as a built-in glyph instead of a text character positioned with a negative margin, so authors no longer need to reset margins when styling it. (321110@main) (185952359)valueAsNumber to throw a TypeError for infinite values and to clear the value when set to NaN, instead of throwing a NotSupportedError for both. (321668@main) (188047421)<input type="search"> and the filter field icons in Web Inspector. (322219@main) (188297101)src changed before its first load committed being missing from the back/forward list, which could show the wrong frame content after navigating back. (321768@main) (149977268)about:blank document to replace the history entry instead of adding a new one, so the back button works as expected. (321285@main) (187298226)scrolling attribute on an <iframe> not taking effect immediately when overlay scrollbars are in use. (321994@main) (188326221)<img> with an empty alt attribute to have no natural size or aspect ratio, so block-level images no longer show a broken-image icon. (321251@main) (155149701)naturalWidth and naturalHeight to return the image metadata size rather than the decoded size; this fixed layout of the image for different sizes. (322220@main) (172477431)image-rendering not applying to generated images, such as cross-fade(), used in the CSS content property. (322533@main) (189024049)naturalWidth and naturalHeight to return the density-corrected natural rather than the layout size which can be affected the style such as image-orientation. (322534@main) (189024238)BigInt("-") and BigInt("0x ") to throw a SyntaxError. (321129@main) (187423917)arguments object truncating its length to 32 bits. (321213@main) (187527803)Error.stackTraceLimit from the Error constructor. (321296@main) (187633441)\k<name>) inside regular expression lookbehinds incorrectly matching the empty string in some cases. (321355@main) (187741552)RegExp.prototype.test() skipping the observable read of lastIndex for some anchored regular expressions. (321363@main) (187754780)ArrayBuffer.prototype.slice() to correctly look up Symbol.species on the constructor, including for ArrayBuffer objects from other realms. (321451@main) (187853060)Intl.DateTimeFormat.prototype.resolvedOptions() reporting a dayPeriod value for 12-hour formats that was never requested, which caused formatters recreated from those options to output text like “in the morning” instead of “AM”. (321443@main) (187860472)Intl.Segmenter containing() returning a segment that also included the preceding segment when the index was at a boundary before a surrogate pair, such as an emoji. (322218@main) (187863002)Intl.DurationFormat dropping zero minutes and seconds that follow a numeric unit. (321755@main) (188252937)Promise.prototype.then(), catch(), finally(), Promise.resolve(), and Promise.reject() creating promises and errors in the wrong realm when used with a Promise from another realm, such as an iframe. (321864@main) (188345846)(a?.b)(x) skipping the call and returning undefined when a is null or undefined, instead of evaluating the arguments and throwing a TypeError. (321867@main) (188347937)Intl.DurationFormat dropping a fractional value folded into a zero unit, so formatting { milliseconds: 500 } with milliseconds: "numeric" returns 0.5 sec instead of an empty string. (321878@main) (188362824)Intl.DurationFormat dropping the minus sign for fractional values between -1 and 0. (321988@main) (188478180)String.prototype and RegExp.prototype methods such as match(), replace(), split(), and test() incorrectly running with the caller’s realm when used with a RegExp or function from another realm. (321990@main) (188478512)this value. (322141@main) (188677696)super property as the tag so the tag function is called with the current this value. (322253@main) (188790013)Proxy that wraps an array-like object. (322260@main) (188794129)ArrayBuffer constructor to throw a RangeError for an invalid length before reading the prototype of newTarget. (322281@main) (188822488)Array constructor to read the prototype of newTarget before throwing a RangeError for an invalid length. (322282@main) (188822747)transfer option in WebCodecs EncodedAudioChunk, EncodedVideoChunk, AudioData, VideoFrame, and ImageDecoder constructors, allowing ownership of ArrayBuffer data to be transferred instead of copied. (321548@main) (187834056)hevc option in VideoEncoder configurations so that WebCodecs can produce HEVC video in Annex B format. (322385@main) (188387670)mode was set with JavaScript could be overridden by automatic caption selection, preventing multiple text tracks from showing at the same time. (321838@main) (183472153)VideoFrame being flipped vertically instead of horizontally when its rotation is applied. (321697@main) (187300986)play() promise never resolved and the playing event never fired for Media Source Extensions content mixing clear and encrypted media when the decryption key was already available. (321495@main) (187850389)VideoEncoder and VideoDecoder hanging on flush() when the underlying codec buffers more than four frames. (321682@main) (188165426)document.cookie or cookieStore.set() getting truncated, and changed reading them to decode as UTF-8 to match other browsers. (322447@main) (124266311)data: URL navigation did not open in Wallet. (322096@main) (184582548)*. (321835@main) (188260300)::before and ::after rules that cannot generate content, such as box-sizing resets, improving performance. (322517@main) (186807298)flex-basis inside deeply nested column flex containers could make layout time grow exponentially and freeze the page. (321866@main) (187727635)ch units and tab-size. (321206@main) (55307109)min-width: min-content or min-width: max-content. (321332@main) (187621931)white-space: nowrap content containing spaces overflowed the line instead of wrapping to the next line. (322209@main) (188406135)line-clamp clamp point. (322345@main) (188471367)clear() on a pattern’s patternTransform.baseVal so the patternTransform attribute is ignored. (321124@main) (170518953)getComputedTextLength() and the character position methods to account for lengthAdjust="spacingAndGlyphs". (321445@main) (187473448)viewBox had zero width as a flex item. (321446@main) (187794543)transform attribute on the outermost <svg> element being ignored or applied in the wrong position. (321441@main) (187854814)pointer-events values stroke, visibleStroke, visible, and all missing the stroke area of SVG shapes when stroke is none. (322143@main) (188285211)navigator.hardwareConcurrency returning unusual core counts to known fingerprinting scripts, which could trigger bot detection challenges, by alternating between 4 and 8 instead. (321431@main) (177211562)securitypolicyviolation events or notifying a ReportingObserver. (322099@main) (188368580)dispose events for entries removed by another frame’s navigation seeing stale navigation.entries(), and fixed a crash when a frame is detached during a dispose event. (321726@main) (184961466)readwrite and a readonly IndexedDB transaction in flight could leave that database permanently unable to run any transaction. (321564@main) (187349025)window.outerWidth and window.outerHeight returning 0 in some apps that embed WebKit. (321417@main) (187603389)pagehide, visibilitychange, and unload event handlers having no effect, such as beacons and messages never arriving, when navigating to a cross-site page that loads in a different process. (321626@main) (188113419)Blob data, such as with Blob.arrayBuffer() or FileReader, by no longer storing completed reads in the memory cache. (321948@main) (188322145)preventDefault() in a navigate event handler did not cancel a cross-site navigation.navigate(). (322591@main) (188636761)webRequest events not firing for subframes loaded inside an extension’s action popup. (321290@main) (167403221)webRequest event listeners filtered by types so that main_frame and sub_frame match top-level and iframe loads respectively instead of being swapped. (321074@main) (187214709)webRequest events reporting the wrong parentFrameId for subframes whose parent is the main frame, which now correctly report 0. (321079@main) (187227773)window.close() did not close the document. (321974@main) (188408957)border-radius: calc(infinity * 1px), overflowing the box model diagram. (322462@main) (174246956)browsingContext.captureScreenshot command, which captures the page viewport as a PNG. (321353@main) (145987196)key and code values, and so that keypad keys on iOS report Numpad* codes and the numpad key location. (322405@main) (187036197)texImage2D() and texSubImage2D() uploads from image sources handled premultiplied alpha incorrectly. (321143@main) (187093618)copyExternalImageToTexture() to preserve alpha values instead of passing lossy premultiplied data, which caused some textures with transparency to render incorrectly. (321601@main) (142087137)createComputePipelineAsync() and createRenderPipelineAsync() never settling when the device was lost or destroyed. (321466@main) (187846864)discard before loops writing NaN values to the render target on some Apple silicon GPUs. (321870@main) (187997601)GPUShaderModule always missed the shader cache, making repeated pipeline creation as slow as a cold compile. (321998@main) (188501171)executeBundles() lost the bind group dynamic offsets after the first execution. (322002@main) (188524116)setBindGroup() on render pass encoders doing redundant sampler registration work. (322005@main) (188530801)With the 2.54 release, WPEPlatform became the default way of integrating WPE WebKit with the underlying platform, and its API is now considered stable. At the same time, the libwpe-based API that WPE has relied on since its beginnings is now deprecated. In this article we explain why this change was made, how applications use the new API, and what is involved in migrating an existing application from libwpe.
Back in 2022 we published an overview of the WPE WebKit project, describing the different components of WPE and how they fit together. Most of what that article explains about WebKit and the WPE API is still valid, but the way WPE connects to the platform has changed considerably since then, so this is a good moment to revisit that part of the picture.
Since its beginnings, WPE has delegated graphics and input to code that lives outside of WebKit. libwpe defined a generic interface for these, and a backend, loaded at runtime, implemented that interface for a given platform. WPEBackend-fdo, based on Wayland and other freedesktop.org technologies, was the reference backend, while Cog provided a convenience layer on top for those writing browsers.
This design achieved its main goal: hardware vendors and integrators could support WPE on their platforms without having to modify WebKit itself. However, it also meant that a good part of the platform work was left to the application. To show a web page on screen using WPEBackend-fdo, an application had to:
wpe_loader_init() and initialize it,
usually by passing it an EGL display.WebKitWebViewBackend, and pass that to the web view.wpe_view_backend_dispatch_*_event() family of
functions.As a result, even a simple browser required a fair amount of knowledge of the graphics pipeline, and the code involved was split across three repositories (WebKit, libwpe, and WPEBackend-fdo) that had to be kept in sync. Much of this code was the same from one application to the next, and Cog provided a ready-made implementation of it for applications that did not want to write their own.
WPEPlatform is a GObject-based library that lives in the WebKit repository and is built as part of WPE WebKit. It replaces both libwpe and the backends written against it, and it moves the responsibility of rendering and input handling away from the application and into WebKit and the platform implementation.
The library is only used by WebKit’s UI process. As before, the web process renders the page into buffers that are shared with the UI process (DMA-BUF buffers on Linux, AHardwareBuffer on Android, or shared memory when rendering in software). The difference is that WebKit now hands these buffers directly to the platform implementation, which takes care of presenting them, without the application having to take part in the process.
The API is built around four classes:
WPEDisplay: the connection to the platform, and the object that
creates all the others.WPEToplevel: a top-level surface, which is normally a window, or the
whole output on platforms that have no windows.WPEView: the surface where a single web view is rendered, hosted in a
toplevel. It also receives the input events for that web view.WPEBuffer: the pixel data produced by WebKit and handed to the view,
with a subclass for each type of buffer.These are complemented by smaller classes for screens, keymaps, settings, input methods, gestures, gamepads, the clipboard, and accessibility. Several of them, such as screens, settings, input methods, and gestures, had no equivalent in libwpe.
WPE WebKit ships with three built-in platform implementations: Wayland, DRM/KMS (for devices that drive the display directly, without a compositor), and headless (for testing and offscreen rendering). Other implementations can be developed out of tree and installed as modules, which WPE WebKit discovers at runtime. wpe-platform-gtk, which embeds WPE web views in GTK 4 applications, is an example of this.
For most applications, WPEPlatform is not visible at all. A
WebKitWebView created without a backend selects a platform
automatically: WPE WebKit tries the available platform implementations in
order of priority and uses the first one that connects successfully. The
following program is a complete, if minimal, browser:
#include <wpe/webkit.h>
#include <wpe/wpe-platform.h>
static void
on_view_closed (WPEView *view, gpointer user_data)
{
g_main_loop_quit (user_data);
}
int
main (int argc, char *argv[])
{
g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE);
g_autoptr(WebKitWebView) web_view = g_object_new (WEBKIT_TYPE_WEB_VIEW, NULL);
WPEView *view = webkit_web_view_get_wpe_view (web_view);
g_signal_connect (view, "closed", G_CALLBACK (on_view_closed), loop);
webkit_web_view_load_uri (web_view, argc > 1 ? argv[1] : "https://wpewebkit.org");
g_main_loop_run (loop);
return 0;
}
It only needs the wpe-webkit-2.0 pkg-config module to build, which
pulls in wpe-platform-2.0 when WPE WebKit is built with WPEPlatform
support (the default since 2.54). Note that the closed signal is
emitted when the user closes the window, and of the built-in platforms,
only Wayland emits it. On DRM and headless the application decides by
itself when to exit.
The platform API comes into play when an application needs more than these defaults. The most common cases are the following.
Choosing a platform. The WPE_PLATFORM environment variable
(wayland, drm, or headless) forces a given platform without any
changes to the code. An application that is meant to run on a single
platform can also create the display itself and pass it to the web view:
#include <wpe/wayland/wpe-wayland.h>
g_autoptr(GError) error = NULL;
g_autoptr(WPEDisplay) display = wpe_display_wayland_new ();
if (!wpe_display_connect (display, &error))
g_error ("Could not connect to Wayland: %s", error->message);
WebKitWebView *web_view = g_object_new (WEBKIT_TYPE_WEB_VIEW,
"display", display,
NULL);
The Wayland-specific API is available with the same wpe-webkit-2.0
module, but an application that depends on it can check for the
wpe-platform-wayland-2.0 pkg-config module, which is only installed
when WPE WebKit is built with the Wayland platform enabled. The DRM and
headless implementations work the same way, with wpe_display_drm_new()
and wpe_display_headless_new().
Handling input. Input events are delivered to the WPEView through
the event signal before they reach the web page. An application can
connect to it to implement keyboard shortcuts, returning TRUE to stop
the event from reaching the page:
static gboolean
on_view_event (WPEView *view, WPEEvent *event, gpointer user_data)
{
if (wpe_event_get_event_type (event) == WPE_EVENT_KEYBOARD_KEY_DOWN
&& (wpe_event_get_modifiers (event) & WPE_MODIFIER_KEYBOARD_CONTROL)
&& wpe_event_keyboard_get_keyval (event) == WPE_KEY_q) {
g_main_loop_quit (user_data);
return TRUE;
}
return FALSE;
}
g_signal_connect (view, "event", G_CALLBACK (on_view_event), loop);
Controlling the window. The toplevel that hosts the view is available
through wpe_view_get_toplevel(), and provides functions to set the
title (wpe_toplevel_set_title()), resize, maximize, or switch to
fullscreen (wpe_toplevel_fullscreen()). Changes in its state are
reported by the toplevel-state-changed signal of the view. How much of
this is supported depends on the platform: Wayland implements all of it,
the headless implementation only keeps track of size and fullscreen
state, and DRM does not support them, since its toplevel always covers
the whole screen.
Finally, wpe_display_get_settings() gives access to WPESettings,
where applications can override platform settings such as the dark mode
preference, reduced motion, or contrast. The prefers-color-scheme,
prefers-reduced-motion, and prefers-contrast CSS media queries
follow these settings.
For an application, most of the migration consists of removing code. With
WPEBackend-fdo, creating a web view looked roughly like this, with most
of the actual work happening in the callbacks of exportable_client:
struct wpe_view_backend_exportable_fdo *exportable =
wpe_view_backend_exportable_fdo_egl_create (&exportable_client, app, width, height);
struct wpe_view_backend *wpe_backend =
wpe_view_backend_exportable_fdo_get_view_backend (exportable);
WebKitWebViewBackend *backend =
webkit_web_view_backend_new (wpe_backend, destroy_backend, exportable);
WebKitWebView *web_view = webkit_web_view_new (backend);
With WPEPlatform, the same is achieved with a single line:
WebKitWebView *web_view = g_object_new (WEBKIT_TYPE_WEB_VIEW, NULL);
webkit_web_view_new() is only available when WPE WebKit is built with
the legacy API, so applications need to switch to g_object_new(). The
rest of the application code that uses the WebKit API (settings, network
sessions, navigation, and so on) does not need any changes. The diagram
below summarizes what moves out of the application:
The rest of the libwpe code in an application maps to WPEPlatform as follows:
| libwpe / WPEBackend-fdo | WPEPlatform |
|---|---|
wpe_loader_init() and backend initialization |
Automatic, or WPE_PLATFORM, or creating a WPEDisplay |
| Exportable client callbacks, buffer release, frame completion | Handled by WebKit and the platform implementation |
wpe_view_backend_dispatch_*_event() |
Handled by the platform; intercept with the WPEView::event signal |
| Fullscreen handler | wpe_toplevel_fullscreen() and the toplevel state |
There are a few things in libwpe and WPEBackend-fdo that have no
equivalent in WPEPlatform. The process provider API of libwpe is gone,
since WebKit is again in charge of launching its auxiliary processes. The
only exception is Android, which has its own WPEProcessManager API. The
audio extension of WPEBackend-fdo has not been supported since 2.46,
and the video plane extension, used to display video frames on hardware
overlay planes, has no equivalent yet.
Those who maintain a custom backend for WPEBackend-fdo, rather than an
application, will need to port it to a WPEPlatform implementation. This
means subclassing WPEDisplay, WPEToplevel, and WPEView, and
optionally the input method, keymap, and screen classes. The tutorial on
writing a platform
implementation
covers this in detail. In many cases, extending one of the built-in
implementations might be enough: the Wayland one, for example, exposes
its wl_display, wl_compositor, and wl_surface objects, which makes
it possible to add support for additional Wayland protocols.
On the build side, the legacy API is still built by default in 2.54 and
will continue to be maintained while applications migrate. Once an
application no longer needs it, it can be disabled at build time with
the ENABLE_WPE_LEGACY_API=OFF CMake option. As for Cog, it will not
have further stable releases beyond the 0.18.x series, so we recommend
that new applications use the WebKit and WPEPlatform APIs directly.
The reference documentation for WPEPlatform has been considerably extended for this release, and is the best place to continue from here:
For an example of a platform implementation that lives entirely outside of WebKit, Alex has written about WPE WebKit on Android with WPEPlatform, which shows how far the API can be taken.
If you run into problems while migrating, or find that something you relied on in libwpe is missing, please let us know, either by filing a bug in WebKit’s Bugzilla, on the webkit-wpe mailing list, or in the #wpe Matrix channel.
Update on what happened in WebKit in the week from September 28 to October 5.
Another week populated with updates! This time we sweep through a new crypto backend, more layer-based SVG engine updates, better filters, and some miscellaneous updates.
Add moving steps handling to map, img, label and radio input elements to handle their internal book keeping. This also handles custom element registries.
Initial support to build WPE and WebKitGTK with the OpenSSL crypto backend has landed. This is still in development and not yet intended for use, but progress on bringing it to parity with the libgcrypt backend is ongoing.
Fixed stacked backdrop filters in the Skia compositor, so an element with a backdrop-filter placed over an earlier element with its own backdrop-filter now shows that earlier element with its filter applied, instead of without it.
Removed redundant save/restore pairs when painting transformed SVG shapes in the Layer-Based SVG Engine (LBSE), so each shape now uses a single pair, just like the legacy SVG engine. When DOM rendering happens in the GPU process, every state change is an IPC message, so this cuts LBSE from about 8.5 to about 6.5 messages per shape and closes the gap to the legacy engine.
Fixed transformed <clipPath> elements that use clipPathUnits="objectBoundingBox" in the Layer-Based SVG Engine (LBSE), whose transform was wrongly applied in bounding box units instead of user space. Rendering and hit testing now apply it in user space, matching the legacy SVG engine.
Fixed hit testing and repainting of non-scaling strokes in the Layer-Based SVG Engine (LBSE), which kept using the old stroke width after the transform of the shape or one of its ancestors changed. A vector-effect: non-scaling-stroke stroke depends on every transform up to the outermost <svg> element, so its cached bounds are now cleared whenever one of those transforms changes.
Fixed the source area of filters and backdrop filters: a filter's offscreen surface only covered the visible part of its layer, so when a layer was partly scrolled out of the viewport, its drop shadow was cut off even where the shadow itself fell inside the viewport. The surface now also covers the area the filter reads from outside the visible part, as in Chromium, and a backdrop-filter blur is mirrored at the border box as the spec requires, which fixes several web-platform-tests.
Fixed viewport clipping of nested <svg> elements with overflow: hidden in the Layer-Based SVG Engine (LBSE), which did not clip descendants that have their own layer, such as a transformed <g>. Containers that clip their overflow now get their own layer, like containers using clip-path or mask already do, so layered descendants are clipped correctly.
Fixed misplaced child layers of SVG elements with a filter in the Layer-Based SVG Engine (LBSE), which were drawn in the wrong position whenever an ancestor layer had a transform applied.
Restored the Direct Rendering Manager (DRM) vertical blank monitor for the legacy WPE API, so rendering is once again paced by the display's actual refresh cycle instead of a fixed 60 Hz timer. Two regressions had silently disabled it: a missing display ID forced the timer fallback, and a shadowed variable broke the display controller lookup.
That’s all for this week!
Update on what happened in WebKit in the week from September 21 to September 28.
Following the stable releases last week, we have the first security advisory covering fixes included in them; the flurry of fixes and polish for the Skia compositor continues; SharedArrayBuffer gets re-enabled, and more.
Fixed a crash on exit in WebKitGTK
and WPE WebKit, caused by the EGL display being torn down in an atexit()
handler while other threads were still using it, by now destroying it on the
main thread once the GPU process has stopped its WebGL thread and the web
process has closed its pages.
Fixed moving steps for source
element to account for the <media> and <model> elements.
The built-in JavaScript/ECMAScript engine for WebKit, also known as JSC or SquirrelFish.
SharedArrayBuffer is now enabled for the WPE and GTK
ports.
SharedArrayBuffer is exposed when the origin is cross-origin isolated. We have
enabled 35 associated cross-origin isolation tests. Initialization was
designed to work with Android service-launched processes, too.
Fixed a bug with 3D-transformed elements that are tilted so far away from the viewer that part of them ends up behind the camera, where WebKit got the visible area of the element wrong and computed the hidden part instead. As a result, the visible part of such elements is no longer left blank in WebKitGTK and WPE—painting and hit-testing works properly now.
Fixed oversized damage regions for 3D-transformed layers that cross the camera plane, by clipping their projected polygon against the clip bounds so the damage now matches what the layer actually paints.
GPU renderer small paths now use Skia distance fields. Bitmap atlas entries depended on the transform and subpixel position, causing animated paths to be re-rasterized and uploaded every frame. With distance field indexed entries we can reuse them across transforms. We are improving MotionMark Suits results on Raspberry Pi 4 and desktop.
Fixed the paint order of preserve-3d
layers that cross the camera plane in
the Skia-based compositor. Layer corners behind the camera used to be flipped
to the far side of the screen when projected—layers were sorted against the
wrong geometry and far layers painted over near ones. Now the part of a layer
that lies behind the camera is clipped away before sorting, which makes
complex 3D scenes like the CSS FPS
demo render correctly.
Aligned the filter surface with the device pixel grid in the Skia-based compositor, so layers with CSS filters that end up at fractional device positions are no longer resampled when the filtered result is drawn. This fixes wrong colors in the outermost rows and columns of pixels of such layers, which now render the same as they would without a filter.
Fixed SMIL-animated viewBox
changes being ignored on the
outermost <svg> element in the Layer-Based SVG Engine (LBSE), bringing it in
line with the legacy SVG engine.
Fixed edge artifacts on scaled or transformed non-repeating background images in the Skia-based compositor, which painted all composited background images as if they repeat, so a thin line of the opposite edge showed up along the border. Only tiles that actually repeat are painted that way now, which removes the yellow line that showed up above the gun in the CSS FPS demo. This fixes the last known artifact in that demo.
Fixed rendering artifacts in
WebKitGTK and WPE WebKit for 3D-transformed layers that extend behind the
camera under a perspective (for example a large element under
rotateX(90deg)), whose re-composited area came out too small because corners
behind the camera were projected onto the wrong side of the frame, and are now
clipped away before mapping.
Fixed touch positions in the GTK
port, which were offset by the
surface transform, the room left for the client-side decoration shadows,
because it was not subtracted from the raw GdkEvent position, so every touch
landed away from the finger unless the window was maximized. The same change
cancels the touch sequences that were stranded when the web view is unmapped
or a dialog was shown, which left the page with a finger that was never
released.
New, modern platform API that supersedes usage of libwpe and WPE backends.
The ENABLE_WPE_PLATFORM build option has been
removed.
The new WPE Platform API, which was enabled by default since 2.54, is now
always enabled.
A new security advisory, WSA-2026-0006, has been published for
WebKitGTK and for
WPE. This covers issues
fixed in the recent 2.54.0 releases, and for the first time includes security
issues fixed in the bundled ANGLE and
Skia libraries. Updating to the latest stable release is
highly recommended.
That’s all for this week!
Safari Technology Preview Release 253 is now available for download for macOS Golden Gate and macOS Tahoe. If you already have Safari Technology Preview installed, you can update it in System Settings under General → Software Update.
This release includes WebKit changes between: 320113@main…321067@main.
aria-keyshortcuts values containing Meta or Alt literally instead of using the macOS terminology Command and Option. (320424@main) (186342070)timeline-scope property. (320183@main) (186261285)timeline-scope instead of one established by an ancestor, which now takes priority. (320223@main) (186265325)timeline-scope hierarchy could remain active instead of yielding an inactive timeline. (320227@main) (186330965)timeline-scope value did not update timelines for animations outside of its hierarchy. (320231@main) (186331685)ViewTimeline constructor did not require a subject parameter. (320969@main) (187200265)CSSContainerRule.conditions. (320762@main) (182257864)safe and unsafe keywords with normal alignment, and change safe behavior for absolutely (and fixed positioned) boxes to keep the box within their original containing block (the viewport). (185952139)sibling-index() and sibling-count() within container query conditions. (320493@main) (186564084)background-size value collapsed the image tile to nothing. (320169@main) (140387662)[class~=foo] attribute selector did not perform as well as an equivalent class selector. (320862@main) (164128575)::selection, did not inherit as a StyleColor, which could prevent values like color-mix(in oklab, teal 50%, currentcolor) from resolving correctly. (320228@main) (184495338)Range with an already-registered highlight never painted. (320355@main) (185173794)box-shadow with a large spread on a wrapped inline element painted outside the element as full-width bands. (320150@main) (185651318)CSS.highlights iterated in hash order instead of registration order after its wrapper was garbage collected. (320343@main) (185754041)corner-shape rendered incorrectly when inner corners intersected. (320287@main) (185931169)aspect-ratio could keep a stale inline size contribution when the grid container shrank. (320117@main) (186101273)vertical-rl writing mode. (320167@main) (186274088)<details> element made its <summary> one pixel shorter. (320461@main) (186413478)attr() stopped invalidating on attribute changes after a view transition. (320420@main) (186467515)unicode-bidi and direction had no effect on an inside ::marker. (320504@main) (186468752)fit-tolerance did not interpolate between <length-percentage> values. (320383@main) (186509338)inline-block element that had no line-height of its own. (320514@main) (186614663)<length-percentage> from a calc() value to a pure <length> dropped the percentage component at 100% progress. (320585@main) (186628152)text-decoration-thickness and text-underline-offset did not preserve percentage values when interpolating. (320584@main) (186628810)align-content left the list marker behind. (320532@main) (186676835)text-indent moved an outside list marker along with the indented text. (320533@main) (186678552)tab-size was small in a proportional font. (320671@main) (186698040)content property painted blank. (320620@main) (186742805)corner-shape with thick borders rendered incorrectly. (320826@main) (186808871)fit-content() grid tracks could be stretched beyond their argument instead of capping the track’s growth limit. (320780@main) (186961437)<caption>‘s margins were missing from the table. (321056@main) (187137359)<caption> ignored its margin against the table edge. (321060@main) (187139068)right and bottom values of an out-of-flow positioned element were wrong inside a vertical inline containing block. (321061@main) (187295559)w component when mapping a 4-component point. (321030@main) (187297446)<canvas> element with no pushed OffscreenCanvas frame would fail instead of returning a transparent black image. (320202@main) (186229685)drawImage prevented its backing surface from being recycled. (320735@main) (186731221)<canvas> computed path bounds unnecessarily when the whole backing store was already marked dirty. (320709@main) (186946552)<select> could render outside the viewport, by applying safe alignment to keep it within its original containing block. (320564@main) (185952139)load event could fail to fire if a readystatechange handler triggered a new load during page completion. (320331@main) (186373617)BigInt values in Intl.PluralRules.prototype.select and Intl.PluralRules.prototype.selectRange. (320396@main) (186534585)BigInt values. (321013@main) (187336927)XMLHttpRequest) to keep using the original value. (320742@main) (180048596)Intl.DurationFormat in digital style included a stray separator when minutesDisplay was set to "auto" and minutes ended up hidden. (320182@main) (180722012)Array.prototype.toSpliced threw a TypeError instead of a RangeError when the array’s length was Infinity. (320554@main) (184438837)TypedArray setFromTypedArray could not use memmove when the region was intentionally overlapping and the spec algorithm needed to read back the modified result. (320130@main) (186145219)TypedArray constructor could produce incorrect results when copying Array content whose element access has side effects. (320185@main) (186227333)String.prototype.toLowerCase and String.prototype.toUpperCase did not scan strings inline in the DFG and FTL JIT tiers. (320293@main) (186301688)/v mode regular expression class incorrectly added U+0000 to the class. (320216@main) (186319779)Temporal.ZonedDateTime.prototype.round resolved the rounded wall-clock time using a minute-rounded offset instead of the correct offset. (320217@main) (186319834)Object.freeze() did not invalidate the megamorphic inline cache epoch, which could cause stale property accesses on a frozen object. (320276@main) (186393610)RegExp.prototype.test did not fast-fail when the input string was shorter than the pattern’s minimum possible match length. (320487@main) (186534079)Atomics.isLockFree() converted its argument with a 32-bit integer conversion instead of ToIntegerOrInfinity, which could cause it to incorrectly return true for large size values. (320715@main) (186610789)data: URL included the entire script in Error.stack instead of a truncated URL. (320770@main) (186614179)RegExp.prototype.test could skip the required read of lastIndex, silently dropping observable side effects. (320582@main) (186792229)eval calls that didn’t resolve to the real eval function, and incorrectly applied inside generator and async function bodies, which could produce incorrect results. (320687@main) (186840477)RegExp.escape incorrectly narrowed supplementary code points to 16 bits. (320682@main) (186962453)\- following a class set operand in a /v mode regular expression class incorrectly threw a SyntaxError. (320683@main) (186962513)RegExp::deleteCode() cleared a regular expression’s cached pattern atom, which could cause incorrect values from static RegExp properties such as leftContext and rightContext after the compiled code was reclaimed while idle. (320817@main) (187104089)String.prototype.at and String.prototype.codePointAt could return an incorrect value for an out-of-bounds or negative index because the JIT compiler could eliminate their bounds check. (321014@main) (187336661)MediaRecorder could hold back a lone video keyframe indefinitely instead of using it to start a new recording segment. (320219@main) (186229843)file: URL. (320685@main) (179999480)Blob range larger than 2GB truncated the read length due to an integer overflow. (320981@main) (187207663)IntersectionObserver observation on pages with many observed elements. (320395@main) (185839711)scroll-timeline name could block the main thread for multiple seconds. (320902@main) (186090970)ScrollingStateTree::insertNode performed redundant work reordering children on pages with many sibling scrolling nodes. (320527@main) (186143008)overflow: hidden, could trigger an unnecessary full-layer repaint. (320296@main) (186422878)z-index inside a stacking context failed to composite correctly when overlapping an externally composited layer. (320473@main) (130595406)<mo> operator dictionary was consulted even when the element contained non-text content. (320730@main) (173078723)display: contents element received redundant updates during style resolution. (320144@main) (186246705)blur() filter did not properly repaint, leaving stale pixels around the edge of the filtered region. (320398@main) (186278957)rowspan="0" attribute was incorrectly clamped to 1. (320324@main) (186420659)<annotation> and <annotation-xml> were not rendered. (320334@main) (186429768)InterpolationQuality, affecting image scaling across canvas, SVG, and PDF rendering paths. (320429@main) (186439811)clip-path or mask could be treated as fully opaque, causing incorrect rendering. (320634@main) (186722177)position: sticky element with -webkit-box-reflect applied could composite its reflection incorrectly. (320712@main) (186812790)path attribute on <textPath>, which lets text follow an inline path without referencing a separate <path> element via href. (321038@main) (185831618)clipPath, mask, and pattern elements incorrectly prevented the resource from being referenced. (320207@main) (19103270)SVGLength resolved ex units by rounding the font’s x-height up to a whole pixel, causing the SVG DOM and getComputedStyle to disagree. (320606@main) (185984854)SVGImage::isAnimating() returned true for SVG images that had no animations. (320839@main) (187106396)CredentialStorage::removeCredentialsWithOrigin failed to purge HTTPS credentials that used the default port. (320968@main) (187210150)QuotaExceededError as a DOMException subclass, exposed on window and worker global scopes. (320856@main) (141586555)scrollWidth and clientWidth returned inconsistent values when the page was zoomed in or out due to rounding. (320773@main) (121879134)URLPattern matching issues affecting Service Worker routing, including incorrect encoding of the username and password components and unhandled unclosed pattern groups. (320122@main) (183434701)about:blank for a browsing context that had not yet navigated. (320428@main) (183468933)WKWebView.magnification reported the pre-gesture scale for a few moments after a pinch-to-zoom gesture ended, instead of the scale about to commit. (320341@main) (185381317)Cache.delete() and Cache.keys() swallowed exceptions thrown while constructing a Request from a URL string. (320524@main) (186636105)TypeError when offsetRay was not specified. (320717@main) (186997100)offscreen API. (318833@main) (184269255)i18n.getMessage() dropped non-string substitution arguments instead of coercing them to strings. (320931@main) (149855718)webRequest.onBeforeRequest reported the resource type main_frame instead of sub_frame for subframe document loads. (321067@main) (167403388)webRequest events did not fire for loads blocked by a declarativeNetRequest rule. (320336@main) (176177851)webRequest events did not fire for page loads created with tabs.create. (320430@main) (177727052)vh, svh, and lvh units resolved to the popover’s minimum height instead of its actual size. (320726@main) (186391990)WebSocket handshake headers were missing when using the Network.framework loader. (320156@main) (185255819)Debugger.globalObjectCleared event from a subframe cleared every target’s script map instead of only the affected target’s. (320787@main) (187042936)WebAssembly.Exception constructor rejected null options and incorrectly accepted non-object options. (320215@main) (186319813)i31.get_s and i31.get_u when the operand’s static type was a bottom reference type. (320578@main) (186660590)script.call_function trimmed error messages incorrectly, cutting off part of the message. (320205@main) (186311752)deleteAllCookies command could report success before cookies had actually finished being deleted. (320440@main) (186410147)ShiftRight and ControlRight, on Cocoa platforms. (320741@main) (186961436)no such element instead of stale element reference for a node reference from a previous document. (320729@main) (187042183)sameSite attribute. (320916@main) (187193380)premultipliedAlpha: false were composited incorrectly. (320650@main) (53509398)canvas as a WebGPU texture had very poor performance. (320765@main) (153642354)GPUDevice.createBindGroup()‘s fast path failed to reuse bind groups containing a GPUBuffer entry alongside an external texture, forcing an unnecessary full rebuild. (320309@main) (186379208)video element using WebM or MSE playback rendered as a black placeholder instead of the video frame. (320427@main) (186470526)primitive-index feature as available on GPUs that don’t support it, resulting in an uncaptured error. (320691@main) (186690340)Update on what happened in WebKit in the week from September 14 to September 21.
Following up to last week, we have a handful of exciting fixes to WebKitGTK and WPE WebKit covering Sysprof, rendering of CSS "filter" and "backdrop-filter", and the Layer-Based SVG Engine. We also have optimizations, stable releases, and new blog posts!
Fixed the pairing of begin and end marks in SysprofAnnotator, which kept only one begin per mark and therefore mistimed recursive, concurrent and overlapping marks, so Sysprof traces of WebKitGTK and WPE WebKit now show correct durations for nested work.
Fixed CSS filter and backdrop-filter rendering on transformed elements in the Skia compositor used by WebKitGTK and WPE WebKit, where drop-shadow always fell in the same direction on screen instead of turning with the element, blur was scaled by neither the transform nor the device scale factor, and perspective was not handled at all. The filtered subtree is now painted with the layer transform on the canvas into a surface in the layer's own plane at a fixed scale, with the filter applied there and the result drawn back through the layer transform, as Chromium does.
Stopped rasterizing the parts of a tile that the clip discards in the Skia backing store used by the GTK and WPE ports. Both the drawn quad and its source rectangle are now cut down to the clip first, so the rasterizer no longer fills pixels that are immediately thrown away when a tile is larger than the clip.
Fixed the transform attribute on the outermost <svg> element, which the legacy SVG engine ignored entirely and the Layer-Based SVG Engine (LBSE) wrongly applied inside viewBox space. The attribute is now turned into the CSS transform property, one CSS function per SVG function, so that it transforms the element's CSS box, matching the behaviour of Chromium and Firefox.
After another six-month development cycle, WebKitGTK 2.54.0 and WPE WebKit 2.54.0 have been published. These are the first stable releases of the 2.54 series, that as a highlight include a completely new compositor based on Skia. As it is becoming tradition with each major release, two blog posts go into details of what's new in WPE WebKit 2.54, and in WebKitGTK 2.54.
This release is a milestone for WPE WebKit: after a couple of years in the incubator, the new WPEPlatform embedding API is now considered stable and recommended for new developments. As part of the stabilization, the WPEPlatform manual now includes not one, but two introductory tutorials, and a migration guide to aid migration from the (now) legacy libwpe-based embedding API.
Note that the libwpe-based legacy API is still available and will be kept in light maintenance mode for the foreseeable future.
Coinciding with the first stable releases, Carlos García has written about the new Skia compositor that is debuting in the 2.54 series. It includes plenty of details on why and how the new compositor has been built to replace the aging TextureMapper, and measurements that demonstrate how it is bringing graphics performance improvements across the board.
At the same time, Nikolas Zimmermann published a companion article about damage handling in the new Skia compositor that explains how the feature it has been rewritten with a focus on correctness while ensuring that it saves doing unnecessary work without incurring on excessive busywork that would cancel the benefits.
That’s all for this week!
WPE WebKit and WebKitGTK 2.54 have been released with a bunch of improvements and new APIs as usual, but there’s one point that kept the Igalia WebKit graphics team busy for the whole cycle: the new Skia-based compositor. The replacement of Cairo with Skia for content rendering has been a success and it’s already well integrated and optimized. We thought we could try to use Skia for the composition too and replace TextureMapper with Skia. TextureMapper was introduced in 2010 for the Qt port and later adopted by other ports. It uses the OpenGL ES API and maintains a collection of shader programs to paint different content. Nowadays TextureMapper is mostly the same code and shader programs, and it’s unmaintained and missing features. However, the performance was good and it has served us really well all these years. So, this time the goal was not to get better results in benchmarks, but to modernize the implementation, reduce the amount of code to maintain ourselves (like all shader programs) and make it easier to implement the missing features and fix existing bugs. This post is a summary of all the work we have done this cycle to implement the new Skia compositor.
The first step was adding an SkiaCompositingLayer class to replace TextureMapperLayer and adapt all the code to use one or the other depending on an environment variable. The initial implementation was based on the TextureMapper one for the things that are common like iterating the layer tree, computing transformations, etc. The way layers produced their contents didn’t change, so we were receiving textures for tiled content, video buffers, WebGL, accelerated 2D canvas, etc. SkiaCompositingLayer created a Ganesh Skia surface to draw those textures using SkCanvas::drawImageRect(). This initial implementation was enough to run the default MotionMark test suite, since it doesn’t use other composition features. Even though performance was not the goal, we had to make sure we didn’t regress. This initial implementation was neutral in MotionMark. We needed tests to implement those features and measure performance at the same time, so we decided to add a new set of tests to MotionMark, just extending the existing tests to require composition, which makes sure that filters, masks, path clipping, transformations, etc. were done by the compositor.
We first tried implementing filters using an intermediate surface like TextureMapper does. It worked, but the MotionMark score in the filters test was much worse. We realized that with Skia we could implement most of the filters without using an intermediate surface. All filter types except blur and drop shadow can be simplified to an SkColorFilter with SkImageFilter::asAColorFilter() which can be implemented without an intermediate surface, just by setting the color filter in the SkPaint we pass to SkCanvas::drawImageRect(). This not only fixed the performance regression, but also gave better results than TextureMapper, which always needs an intermediate surface.
There are two different kinds of masks: image mask, where the source mask is an image already, and clip path, where the mask is represented by a path to be clipped. In TextureMapper both are implemented the same way using intermediate surfaces. The mask is painted into a surface and then the masked layer creates an intermediate surface where its contents are first painted and then the mask contents on top using DstIn blend mode. Skia has APIs that allowed us to implement both cases in a much simpler and more efficient way. In the case of image masks, where we already have an image, we paint the mask contents once and keep it cached, and then the masked layer creates an SkShader for the image mask that is passed to SkCanvas::clipShader() without having to paint into an intermediate surface. Clip path masks are even easier, because we can just take the path we get and build an SkPath we can pass to SkCanvas::clipPath(), without having to paint the mask as an image at all or use any other intermediate surface. Once again, masks were not only easier to implement but they ended up being more performant too.

The implementation of 3D layer contexts is fairly independent of TextureMapper and OpenGL, so we could just take it almost as it was, using SkPath to build the clips and a few other adaptations. We could also fix existing bugs like the z-ordering that has always been broken in TextureMapper.

TextureMapper never supported blend modes and they were easy to implement with Skia just using the SkPaint property for it. This made several layout tests start passing.
After implementing all the features we were at a point in which we had the same or better performance in all tests except for three MotionMark compositing tests that were giving much worse results. Those tests use small layers and give a high result which means we end up adding a lot of layers to the scene before we start skipping frames. The root cause was the large number of layers filling the command queue of Ganesh. Skia Ganesh queues the GL drawing operations instead of sending them to the GPU right away. When the surface is flushed for whatever reason, the queued GL drawing operations are then processed and sent to the GPU. This allows Skia to apply nice optimizations like merging several tasks and reducing the amount of draw operations we end up sending to the GPU. In those tests where a lot of layers are created and painted to the compositor Skia surface the internal command queue ends up being huge too. Processing and analyzing such a long queue to optimize what we send to the GPU required more CPU work than what we save by optimizing the GL draw operations. Skia provides an API that allows us to do the batching ourselves. Since the compositor already has information to decide what operations could be merged together, we could reduce the internal queue size in many cases. We can merge SkCanvas::drawImageRect() operations as long as they share the same color filter, blend modes and sampling options. In the best case scenario we could reduce the whole internal queue to just one operation. This time the change improved the results of those tests getting them to about 93% of the TextureMapper score, but still a bit behind.
The Skia Ganesh backend requires that an SkImage backed by a texture is created for the current thread GrDirectContext, even if it’s borrowing an existing texture. In WebKit all textures are created with a sharing GL context so that they can be accessed and destroyed from different threads with the same sharing GL context. So, for a layer whose content is an image we had to create a texture in the compositing thread to upload the pixels if the image was not accelerated, or for accelerated images get the texture identifier of the image, and then create another SkImage from the compositing thread borrowing the texture for the current GrDirectContext. The Skia Ganesh backend provides an API to create promise images, which can be created from any thread but targeting a specific thread, providing a fulfill callback that will be called on the target thread when the SkImage is first used to retrieve the wrapped texture. This way we can create the SkImage from the main thread for the compositing thread without using OpenGL at creation time. For non-accelerated images we realized we don’t need to manually create the texture and upload the pixels in the compositor, we can just pass the unaccelerated SkImage to the compositor SkCanvas and Skia will handle it internally much more efficiently than we did. And this change improved those compositing tests much further than we expected. The reason turned out to be the batching from the previous section: Skia merges the entries of an image set by comparing texture proxy pointers, and until now we were wrapping the texture in a new SkImage on every frame for every layer, so hundreds of layers drawing the very same image produced hundreds of different proxies that Skia could not merge. Passing the same SkImage every time collapses all of them into a single draw operation, which is the best case we described above. With batched painting and promise images together we could beat TextureMapper significantly.

When we switched to Skia for painting, we kept the threaded rendering model, just using a separate smaller queue for GPU rendering workers. The GPU workers created their own GrDirectContext to paint the layer tiles. The resulting textures were re-wrapped in the compositing thread for the compositor GrDirectContext using fences for the proper synchronization. We knew this was not the recommended way to use Skia Ganesh from multiple threads, but with TextureMapper we had no other option. However, with the Skia compositor we can do it the recommended way by using a single GrDirectContext in the compositing thread and use Deferred Display Lists (DDL) and promise images to paint the tiles. With DDL, GPU workers no longer use GL at all and they don’t need a GrDirectContext, they paint tiles into a display list that records the GL drawing operations, but without touching GL. For image drawing operations recorded into the DDL, promise images are used too. Since this is now all CPU work we can remove the smaller GPU worker queue and use a single queue with more workers. The compositor replays the DDL into an SkSurface that is then passed to the compositor SkCanvas.
This change fixed rendering glitches on Android and was performance neutral for the whole composition suite and for most of the MotionMark tests, but in MotionMark 1.3 at 15fps it cost 29% in Suits and 14% in Leaves, while improving Images by 9%. Correctness and the other benefits of DDL made us accept those regressions.

TextureMapper already supported using damage information to optimize the painting while compositing, but it has always been disabled at run time because there were issues we never managed to fix. With the Skia compositor we decided to start from scratch and properly handle the damage information while compositing to render only the parts of the frame that actually changed. I’m not going to go into detail here because Nikolas Zimmermann has written an amazing blog post about it with all the details.
The Skia compositor is finished and enabled by default in 2.54. Even though it was not the main goal, it performs better than TextureMapper in most of the benchmarks we run: the composition suite we added is 45% faster, and MotionMark 1.3.1 is 35% faster. The exception is MotionMark 1.3 at 15fps with GPU rendering, which comes out flat, because the Suits and Leaves tests are still about 26% and 10% behind due to the deferred display lists trade-off described above.
We are already working on fixing existing issues in composition that we never fixed in TextureMapper. In the main branch TextureMapper is now disabled by default at build time, and support will be removed soon for the GTK and WPE ports. In 2.54 it’s still a run-time decision so if you find any issue with 2.54, you can check if it’s a Skia compositor regression by trying TextureMapper with WEBKIT_USE_SKIA_FOR_COMPOSITION=0 environment variable.


We are already working on further improvements like using promise images for all external textures we have to pass to the compositor. We will explore the possibility of using Vulkan with the Ganesh backend instead of GL and eventually try the new Graphite backend. And of course we will continue fixing any existing issues related to the compositor.
Our Safari release notes have never been as long as they are for this version. The number of features alone rose from 58 to 80 since the first beta in June.
Safari MCP makes working with coding agents dramatically easier. Customizable select turns the real <select> element into something you can fully restyle — now with new UA default styles that provide an even-better starting place. Scroll anchoring stops content from jumping when something loads in above. The <model> element comes to iOS, iPadOS, and macOS, giving 3D a powerful HTML element. Websites can now provide immersive environments on visionOS. And much more.
Are you developing websites using coding agents? The Safari MCP server, now available in Safari 27.0 will make your workflow faster and more powerful. Give Claude Code, Codex, or the agent of your choice control over the browser window so it can see how your code renders. Safari MCP provides access to the DOM, network requests, screenshots, and console output. Your agent can do more on its own while you do less hopping between windows, less dropping screenshots in your terminal, and less typing prompts to describe what’s not working.
The Safari MCP server enables your agent to:
And much more. The MCP server runs entirely on your local machine. It makes no network calls of its own. It does not have access to your personal information in Safari. And any captured data goes directly to the agent you’re running, not to Apple.
To give it a try, go to Safari > Settings > Developer > check “Allow remote automation and external agents.” (If the Developer pane is not available, first go to Advanced, and check “Show features for web developers”.)
If you’re using Claude:
claude mcp add safari-mcp -- "/usr/bin/safaridriver" --mcp
If you’re using Codex:
codex mcp add safari-mcp -- "/usr/bin/safaridriver" --mcp
For other agents, you put the following in your mcp.json or config.json file.
{
"mcpServers": {
"safari-mcp": {
"command": "/usr/bin/safaridriver",
"args": ["--mcp"]
}
}
}
Let us know what you think, especially if you have any feature requests. Learn more by reading Introducing the Safari MCP server for web developers, or by referencing documentation.
The biggest feature of Safari 27.0 isn’t a feature at all. It’s the tremendous effort that went into improving the quality of existing features. At WWDC, we were proud to announce 525 fixes. Then we added 60% more, reaching a total of 844. Plus the majority of feature work improves existing features.

When we look at the efforts we made to improve quality, the story can be seen in several themes.
Compatibility. Our team made many changes to help make specific websites work correctly for their users. For example, Hindi InScript typing in an online document editor, images vanishing from search results on a restaurant reservation site, and Pahawh Hmong text misrendering in an online encyclopedia.
Foundations. Sometimes the best way to improve quality is to start over. Safari 27.0 has an all-new ES module loader. We rebuilt CSS Zoom. And now inline layout places elements with subpixel precision.
Depth. We got deep into specific technologies. There are 66 fixes to SVG in this release alone, including an end-to-end review of the SMIL animation engine. HTML tables got a systematic pass, with absolutely positioned tables now handling percentage-sized children, min-height, and max-height correctly. Plus deep work on Media Source Extensions (MSE) and Encrypted Media Extensions (EME). And much more.
Alignment. Much of the work is to better match exactly what web standards prescribe. For example, we corrected the MathML Core operator dictionary and its spacing values across several fixes. Fixes to innerText bring Safari’s rendered-text output better in line with standards for display, visibility, white-space, and form controls. And we improved how HTTP cache obeys Cache-Control.
Integration. Sometimes two features each work perfectly alone, but combined, something starts to go wrong. We fixed a lot of these this year. For example, -webkit-line-clamp shipped in WebKit in 2010, while text-wrap: balance arrived in 2024. Before Safari 27.0, if you applied both to the same element, the balancing simply didn’t happen. Now that’s fixed.
We truly hope all of these efforts throughout the last year make your work as a web developer a little easier. Read through the resolved issues at the end of this article to see specifics. And learn more about what we are doing to raise the quality of WebKit by watching What’s new in WebKit for Safari 27.
The <select> element has been part of the web since the very beginning of HTML. But until recently, there wasn’t a lot you could to do style it or fill it with custom content. Customizable Select changes that. Now in Safari 27.0, it lets you build a fully custom drop-down menu to match the look and feel of your website or web app, without reaching for JavaScript or a pile of <div>. You can even push far beyond a typical drop-down menu to a very different UI. Because it’s a real form control, you get automatic, reliable support for keyboard navigation, screen readers, form submission, validation, change events and more.
Start by applying appearance: base-select in your CSS. This immediately switches to the look and feel provided by new UA styles, and enables the new powers in HTML.
select,
select::picker(select) {
appearance: base-select
}
You might notice that the default UA styles in Safari 27.0 are different than they were for the first beta back in June. The summer gave us the opportunity to reflect on what it will be like for web developers to write custom styles on top of the new defaults. We realized after 30 years of web developers struggling with form control styling, we wanted to provide something even better.

These defaults set you up with all the basics. You won’t be left with homework to do to get the select into a usable state. You can simply switch to the new control with appearance: base-select, and apply as little or as much additional code as you’d like. Don’t like the new defaults? You are in luck, it’s very easy to override them. Feel fine keeping any of these pieces like the new drop shadow, 4px rounded corners, touch-friendly line height, cleaner hover states, user-ready chevron & checkmark, subtle opt group styling, etc? Great! It’s already done for you, with support for all the variations like light & dark modes, forced color mode, disabled states and more.
We brought this new design to the CSS Working Group, where it’s being further discussed and refined. Once other browsers update their implementations, we will together reach our shared commitment for all browsers to support an identically interoperable starting place.
New pseudo-elements like ::picker-icon and ::checkmark let you easily target parts of the control that were previously unstylable. Plus, you can now insert HTML elements inside each <option> to add more detail. The new <selectedcontent> element can be used to adjust what gets displayed as the currently-selected option’s content. Learn more watching Rediscover the HTML Select Element from WWDC26.

Originally shipped a year ago in visionOS, the HTML <model> element is now also available in Safari on iOS, iPadOS, and macOS. This new element is a lot like video, audio, and img — this time embedding a 3D model in the page.
<model src="mallet.usdz"></model>
Just like the other HTML elements for media, you can link to multiple source files, including a fallback.
<model>
<source src="boot.usdz" type="model/vnd.usdz+zip">
<source src="boot.glb" type="model/gltf-binary">
<img src="boot.png" alt="workboot in light tan leather">
</model>
You can optionally include attributes like environmentmap to provide custom lighting for your model. Or stagemode, which sets the default interaction behavior. Target your model with JavaScript and open up a wide range of possibilities.
Learn all about it, including where to get a 3D model, how to optimize it for the web, and what can be done with JavaScript by watching Get started with the HTML Model Element from WWDC26. And check out these demos in Safari.
Safari 27.0 also adds support so the CSS dynamic-range-limit property can be applied to the <model> element, giving you control over HDR tone mapping and rendering range for 3D content on iOS and macOS.
Responsive image techniques get easier with the auto keyword for sizes.
<img src="photo.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w"
sizes="auto"
loading="lazy"
width="1200"
height="800"
alt="A mountain landscape">
Using sizes="auto" on an image with loading="lazy" tells the browser to automatically calculate the size based on the actual layout width once it’s known. This means you don’t have to predict the rendered layout width ahead of time.
Safari 27.0 adds support for the shadowrootslotassignment attribute on declarative shadow roots. This lets you configure the slot assignment mode (named or manual) directly in HTML when defining a shadow root declaratively, matching the JavaScript attachShadow({ slotAssignment: "manual" }) option.
Environments in visionOS are an incredible part of the experience of Vision Pro. They let you transform your physical surroundings into a different place—like Yosemite, Mount Hood, or the Moon. It’s been possible for Apple developers creating immersive apps for visionOS to provide custom environments with their app. Now in Safari 27.0, environments can be provided as part of a website.

You can provide an immersive environment with a simple <model> element and one JavaScript API call. The Immersive API on the model element works similarly to how the Fullscreen API does on video elements. Learn all about it in Explore immersive website environments in visionOS.
By the way, this new Immersive API replaces the developer preview originally called Spatial Backdrop. If you built anything using Spatial Backdrop, migrate it to the Immersive API on the <model> element.
Now the <img> element has a controls attribute in HTML. It works just like the controls attribute on the video and audio elements. When present, the browser offers controls to allow the user to adjust or more fully experience the media.
<img controls src="panorama.jpg" alt="A panorama of the Dolomites" >
In Safari 27.0 in visionOS when the controls attribute is present, Safari provides a user interface for interacting with spatial and panorama photos. This gives users an easy and consistent mechanism to view photos spatially or immersively, and eliminates the need for web developers to build their own UI.

Safari 27.0 adds support for texture array projection layers in WebXR Layers. When creating a projection layer with XRWebGLBinding.createProjectionLayer(), you can now request textureType: "texture-array" so each eye’s view renders into its own layer of a single texture array.
Many websites inject content into the page as the user is reading or viewing that content. The new content often appears above where the user is currently looking — like images, ads, or comments being injected into the page. In the past, this caused the content the user was reading to be suddenly pushed down, causing a disorienting jump to a random place on the page.
Now with support for Scroll Anchoring, Safari 27.0 instead adjusts the scroll position and keeps the content exactly where it was before the content insertion. As a web developer, you don’t have to do anything to enable this on your site. It just works.
Scroll anchoring is controlled by the overflow-anchor CSS property, which defaults to auto. If you have a specific need where you need to opt out of scroll anchoring, you can use overflow-anchor: none.
Safari 27.0 adds support for using stretch with the properties width, height, min-width, max-width, min-height, max-height, and flex-basis. The stretch keyword tells an element to fill the available space in the relevant axis.
.card {
width: stretch;
}
It’s just like using width: 100% — but this time accounting for margins, which prevents overflow. If you’ve been using -webkit-fill-available to solve this need, now is a good time to switch.
Safari 27.0 makes three updates to anchor positioning, as the web standard evolves and the tool becomes more powerful.
First, we added support for transform-aware anchor positioning. Now, when an anchor element has a CSS transform applied — scale, rotate, translate, or any combination — elements positioned relative to that anchor follow its transformed position instead of its pre-transform layout position. This works for transforms applied via the transform property as well as through the individual translate, rotate, and scale properties. If you use anchor positioning to attach a tooltip, popover, or annotation to a transformed element, it now tracks correctly, even with animated transforms.

Second, the default value for position-anchor changes from auto to normal, fixing a potential side effect where the positioning behavior of elements that don’t even use Anchor Positioning could be impacted. The new value none opts out entirely. The new default, normal, behaves the same as none unless position-area is also set, in which case it behaves like auto did, as originally intended.
And third, Safari 27.0 also adds support for anchor-valid and anchor-visible . Originally, position-visibility: anchors-valid hid an element if any of its required anchor references couldn’t be resolved. However, it wasn’t clear what constituted “required anchor references”. So the CSS Working Group changed the behavior to only look at the default anchor box. To match, some keywords were renamed to drop the plurality. The anchors-valid value is now anchor-valid , while anchors-visible is now anchor-visible. Safari 27.0 aligns with the new behavior, and temporarily supports the old keywords for compatibility.
The new alpha() relative color function is a shorthand for adjusting just the alpha channel of an existing color, without repeating the rest of its channels: alpha(from var(--mycolor) / 80%). It keeps the origin color in its own color space and only changes the alpha value — useful when you want a more transparent or more opaque version of a color you already have, without writing out the full relative color syntax.
The color-mix() function now accepts more than two colors, so you can blend several colors together at once, like color-mix(in oklab, teal 20%, olive 30%, blue 50%). If you leave out the percentages, each color contributes equally.
The image(<color>) function lets you use a solid color anywhere an <image> value is expected. Unlike background-color, which sits underneath all background layers, image(<color>) behaves like a real image layer — it can stack above other background images, get sized with background-size, and be positioned and clipped like any image.
Safari 27.0 also adds support for forwarding missing color components when interpolating between analogous color spaces. Previously, a color with an intentionally missing component (none), like an achromatic gray with no meaningful hue, could get incorrectly assigned a hard 0 when converted into an analogous space for interpolation, producing a subtly wrong blended color. Now the missing component is carried forward as missing instead, so interpolation behaves the way you’d expect.
The light-dark() function now accepts <image> values, not just colors, so you can specify different images for light and dark color schemes in a single declaration: background-image: light-dark(url(day.png), url(night.png)). Gradients work here too.
Safari 27.0 adds support for the :heading pseudo-class, which matches any heading element — <h1> through <h6>. Instead of writing h1, h2, h3, h4, h5, h6 in your selector list, you can just write :heading. Plus, :heading also has a functional form for targeting specific levels, for example, :heading(1, 2) matches only <h1> and <h2>.
The revert-rule keyword is now supported in Safari 27.0. Like revert and revert-layer, revert-rule rolls back the cascade — but specifically to the state as if the current style rule had not been present. It gives you a more precise tool for working with overrides, especially in component libraries and design systems where you want to selectively undo declarations within a rule without losing the rest.
The CSS progress() function now supports a no-clamp option in Safari 27.0. By default, progress() returns how far a value sits between two bounds as a ratio from 0 to 1, clamped to that range. Adding no-clamp removes the clamp, so the result can fall below 0 or above 1, which is useful when you want an effect to keep scaling past its defined bounds instead of flattening out at the edges.
Safari 27.0 adds support for contain: style applying to CSS quotes. This allows you to scope effects of quotes to a certain subtree.
Safari 18.4 added support for text-autospace to control spacing between Chinese/Japanese/Korean (CJK) and non-CJK characters. Safari 27.0 now adds the insert keyword, making text-autospace: ideograph-alpha ideograph-numeric and text-autospace: ideograph-alpha ideograph-numeric insert equivalent.
The Dutch IJ digraph is now supported in Safari 27.0. When the content language is Dutch (lang="nl"), text-transform: capitalize and ::first-letter now correctly titlecase “ij” to “IJ” at the start of words.
Safari 27.0 adds support for the case-sensitive s modifier in CSS attribute selectors. Adding s after the value forces a case-sensitive match — for example, a[href$=".PDF" s] matches only a literal uppercase .PDF. This is the counterpart to the i modifier you may already be using to force case-insensitive matching (a[href$=".pdf" i] matches .pdf, .PDF, .Pdf, and so on); s lets you go the other way when you need an exact-case match on an attribute HTML would otherwise treat as case-insensitive.
Safari 27.0 also adds support for the :host:has() compound selector, letting a shadow host style itself based on what’s inside its own shadow tree. Because :has() can compound onto any selector, :host:has(:checked) or :host:has(::slotted(img)) let a custom element’s host change its own appearance depending on the state of its shadow content — useful for web component authors who want the host to react to what’s inside it without reaching for JavaScript.
Safari 27.0 adds the animation property to the AnimationEvent and TransitionEvent interfaces, letting event handlers directly access the Animation object associated with the event.
Safari 27.0 adds quite a few improvements to SVG.
Now the lang and xml:lang attributes are supported inside SVG. Use it to specify the language of text content to ensure correctness of both text rendering and accessibility announcements.
Safari 27.0 adds support for <use> referencing an external SVG file without a # fragment identifier. Previously, in order to point <use href="…"> at another SVG document, you had to name a specific element inside it with a fragment but now <use> can reference the external file on its own. There’s also a fix so <a> elements in SVG are treated consistently with HTML <a> elements for origin/security checks.
Several non-standard and legacy SVG interfaces have been removed to better align with the SVG 2 specification:
SVGLocatable and SVGTransformable interfacesnearestViewportElement and farthestViewportElement properties on SVGGraphicsElementviewTarget property on SVGViewSpecglyph-orientation-horizontal propertyPlus, there are a huge number of SVG fixes shipping this year. See the list below for what’s improved in Safari 27.0.
Safari 27.0 adds support for WebAssembly JavaScript Promise Integration (JSPI). JSPI lets synchronous-looking WebAssembly code suspend and wait for JavaScript Promises, making it much easier to port existing C, C++, Rust, and other language code to the web where that code expects synchronous I/O.
Before JSPI, porting code that called synchronous APIs to Wasm required rewriting everything on top of a callback or async state machine. With JSPI, the Wasm module can suspend at a call site and resume when the Promise resolves — the rest of the module sees straight-line synchronous code. This is a significant capability for the Wasm ecosystem.
Safari 27.0 includes a complete standards-compliant rewrite of the ECMAScript module (ESM) loader. The new loader is implemented in native C++ and conforms directly to the ECMAScript specification’s module loading algorithms, replacing an earlier implementation based on an abandoned 2016 WHATWG Loader proposal that predated top-level await entirely.
The rewrite fixes module execution ordering and initialization issues that could cause imports to access exports before they were fully evaluated. It was validated against test262, the Web Platform Tests, and additional test cases.
Top-level await is a foundational feature of modern JavaScript module authoring, and it’s been a real pain point in Safari for a while — a known source of cross-browser bugs that developers building module-based apps had to work around. This fix closes that gap. To learn more, read Fixing Top-Level Await in Safari.
Safari 27.0 adds support for the Service Worker static routing API. This lets a service worker declare routing rules that the browser can use to bypass the service worker entirely for certain requests, reducing overhead for high-performance PWAs.
Safari 27.0 adds three improvements to ReadableStream. First, the async iteration with for await...of:
const response = await fetch("/data");
for await (const chunk of response.body) {
process(chunk);
}
Second, the ReadableStream.from() static method for creating a stream from any async iterable or iterable:
const vegetables = ["Carrot", "Broccoli", "Tomato", "Spinach"];
const asyncIterator = (async function* () {
yield 1;
yield 2;
yield 3;
})();
// Create ReadableStream from the array
const myReadableStream = ReadableStream.from(vegetables);
// Create ReadableStream from async iterator
const asyncReadableStream = ReadableStream.from(asyncIterator);
And third, the ability to transfer a ReadableStream , WritableStream and TransformStream across contexts via postMessage().
Several Web Inspector updates in Safari 27.0 make common debugging tasks easier.
The Color Picker now shows color contrast information inline as you edit. No more switching tools mid-decision to check whether a color combination is accessible. This works when you’re editing both foreground and background colors at the same time.

The Color Picker’s format and gamut controls are also now visible upfront instead of hidden. If you’ve ever gone hunting for those options, this will help.

In the Network tab, when a resource redirects, you can now see every request in the chain rather than just the final destination. It’s much easier to figure out what’s actually happening.

The Elements tab adds Subgrid and Grid-Lanes badges that make it easy to identify subgrid and grid-lanes layout contexts as you explore a page.

The Timeline tab now includes the layout root element in Layout event details, so you can see which element triggered a layout pass. The Timeline view also uses distinct colors for style events like “Style Invalidated” and “Style Recalculated”, making them easy to tell apart from layout events at a glance, and adds a separate column showing the node associated with each layout and rendering event.
Safari 27.0 supports setting TextTrackCue.endTime to Infinity to represent an unbounded cue duration. It’s useful for captions or data cues of live streams.
Safari 27.0 adds support for synchronized video playback on macOS displays using genlock. Genlock synchronizes the timing signal across multiple displays or capture devices, which is important for broadcast, live-event, and multi-display installation setups. Even a few frames of misalignment between screens is visible. When genlock is available, video played in Safari stays in lockstep with the rest of the signal chain instead of drifting on its own clock.
Safari 27.0 improves how HDR images with gain maps are decoded and rendered, decoding them into accelerated backing stores in the GPU process. This is a rendering-pipeline improvement rather than a new API — HDR photos with gain maps (the format used by iPhone’s Adaptive HDR photos) should render more correctly and efficiently, building on gain-map rendering fixes from recent releases.
Safari 27.0 lets you override the color space a hardware VideoDecoder uses when decoding video with WebCodecs. This helps when a stream’s embedded color space metadata is missing or wrong. Now, you can tell the decoder which color space to interpret the frames in instead of being stuck with an incorrect result.
Safari 27.0 adds support for Secure cookies on loopback hosts. For loopback hosts using plaintext HTTP, cookies marked Secure can now be set via JavaScript and Set-Cookie headers, matching the behavior of other browsers, simplifying local development and testing with Secure cookies.
Safari 27.0 now supports the clip_distances built-in value in WGSL shaders. Clip distances are a WebGPU feature that allows vertex shaders to define custom clipping planes, enabling you to discard geometry on one side of an arbitrary plane before rasterization occurs.
The radii argument of CanvasPath.roundRect() is now optional in Safari 27.0. Calling roundRect(x, y, width, height) without radii draws a plain rectangle with square corners which is the same as calling rect() , matching the behavior of other browsers.
Safari 27.0 adds srgb-linear and display-p3-linear to predefined color spaces, making these linear-light color spaces available in Canvas, WebGL, and other APIs.
Safari 27.0 updates its MathML operator dictionary to match the MathML Core specification, adding support for multi-character operators (like ++, :=, and /=). This improves spacing and layout for these operators in complex mathematical notation.
In addition, Safari 27.0 supports tabindex, focus(), blur(), and autofocus on MathML elements, improving MathML feature parity with HTML. This makes math content fully participate in keyboard navigation and focus management, which supports interactive educational content and accessibility.
Safari 27.0 also adds support for detecting embellished operators through <mrow> for underover layout. In MathML, an operator wrapped in a grouping element like <mrow> — for example, <mrow><mo>∑</mo></mrow> used as the base of a <munder> or <munderover> — still counts as that operator for layout purposes.
And the href attribute is now deprecated on all MathML elements except <a>, matching how HTML already restricts href to elements built for navigation rather than treating it as a global attribute.
Safari 27.0 adds support for the targetLatency attribute in WebRTC, for specifying a target latency on a receiver. It adds support for the RTCRtpCodec dictionary and related constructs, improving the ability to inspect and configure codecs. It adds support for RTCRtpReceiver.jitterBufferTarget, for tuning the jitter buffer. And it adds video source width and height to RTC stats.
Safari 27.0 now supports specifying maxAge when setting a cookie via the Cookie Store API.
await cookieStore.set({
name: "session",
value: "abc123",
maxAge: 60 * 60 * 24 * 7, // one week, in seconds
});
Safari 27.0 now supports menu items that convert editable text between Simplified and Traditional Chinese characters. It’s available in the “Transformations” submenu of the context menu for relevant text selections.
Safari 27.0 adds WebDriver support for the Digital Credentials API, with commands that let automated tests simulate wallet payloads, wait indefinitely for a credential response, and simulate a user rejecting the request. This lets you write end-to-end tests for Digital Credentials flows without needing a real wallet or person to drive the interaction.
The runtime.getDocumentId() Web Extension API now has support in Safari 27.0. It adds reporting of uncaught JavaScript exceptions and unhandled promise rejections in Web Extension scripts, making extensions easier to debug. It adds support for propagating user gestures through sendMessage(), connect(), postMessage(), and executeScript() — so extensions can reliably perform actions like media playback that require user activation. And it adds support for the tabId key in chrome.windows.create(), letting an extension move an existing tab into a newly created window instead of only creating new tabs from scratch.
Safari 27.0 helps native app developers do even more using the WKWebView public API for native app developers. Build advanced browser and web-hosting experiences on top of WebKit more easily with the following new features:
WKJSHandle — use JavaScript object references from native code.WKContentWorldConfiguration — configure content world properties such as autofill scripting, shadow root access, and inspectability when creating a WKContentWorld.alternateRequest and overrideReferrerForAllRequests on WKWebpagePreferences — modify the main resource request during navigation and apply custom referrer headers across all resource loads.willSubmitForm callback on WKNavigationDelegate — receive notification of HTML form submissions via a new WKFormInfo object.mainFrameNavigation on WKNavigationAction and mainFrameNavigation on WKNavigationResponse — correlate navigation actions and responses with each other and their originating loads. WKWebView.load(_ url:) — load a URL directly without wrapping it in an NSURLRequest. WKDOMNodeSnapshot — clone DOM nodes, including shadow roots, between different WKWebView instances.WKHTTPCookieStore.cookies(for:) — retrieve cookies matching a specific URL without fetching the entire cookie store.WKWebpagePreferences.globalPrivacyControlEnabled — let a native app enable or disable sending the Global Privacy Control (GPC) Sec-GPC HTTP header on outgoing requests for a given page load. speechSynthesis.cancel() removed utterances queued by subsequent speechSynthesis.speak() calls. (46151521)<use> elements referencing <symbol> elements inside an <img> were incorrectly included as unnamed images in VoiceOver’s Images rotor. (98999595)id attribute of an element targeted by aria-owns did not update the accessibility tree. (107644248)aria-labelledby to correctly use their assigned slotted content for accessible names and ignore hidden slotted nodes. (114500560)<meter> element to have consistent labels between aria-label and title attributes. (127460695)display: contents and content in a shadow root to have their content properly read when referenced by aria-labelledby. (129361833)aria-labelledby to use the checkbox name instead of its value when the checkbox name comes from an associated <label> element. (141564913)drawFocusIfNeeded() canvas API. (146323788)role="presentation". (159304061)<details> elements was not exposed in the accessibility tree. (159865815)contextmenu event was not fired for elements inside iframes when triggered by keyboard or assistive technology actions such as VoiceOver’s VO+Shift+M. (164128676)<input type="button"> elements inside live regions were not announced by assistive technologies. (168200460)::first-letter text not being exposed in the accessibility tree when no other text accompanies it. (168458291)aria-owned rows and their cells in grids and tables. (168770938)<svg> named by a child <title> element did not expose an accessible name. (172559238)aria-activedescendant, preventing assistive technologies from interacting with list items. (172931277)hidden="until-found". (173228707)aria-owns was not respected when computing the accessible name from element content. (173249317)focus() calls for newly added elements. (177167634)<a> element with a click handler but no href not being exposed as a link. (179398579)aria-labelledby when the referenced element dynamically changes its aria-label. (180319221)animation-fill-mode did not correctly apply viewport-based units after the viewport was resized. (80075191)!important declarations did not override CSS animation values when CSS transitions were also running on the same property. (174367827)-webkit-text-fill-color incorrectly overrode text-decoration-color. (47010945)shape-outside computing incorrect text wrapping in RTL writing modes. (56890238)aspect-ratio intrinsic-size handling in flex layout to align with the specification. (83240099)flex-basis instead of the specified value for definiteness evaluation. (85707621)outline: auto on macOS. (94116168)box-shadow did not work on display: table-row elements. (96914376)text-indent with calc() containing percentages to correctly treat percentage components as zero for intrinsic size contributions. (97025949)fit-content. (97492632)clip-path: inset() border-radius values did not render correctly at certain element and clip-path sizes. (110847266)text-decoration-thickness propagation to inner spans with non-inline style. (111015539)-webkit-box flexbox emulation not sizing children correctly inside <fieldset> elements. (114094538):where and :is selectors. (114904007)display: table could have incorrect layout when borders were present. (116110440)aspect-ratio not being respected on flex children when the flex container has position: absolute. (117807518)aspect-ratio not working correctly on flex children that also have overflow set. (118926827)font-family serialization to preserve quotes around family names that match CSS-wide keywords or generic families. (125334960)border, position: absolute, and aspect-ratio: 1 were not rendered as squares. (126292577)perspective-origin failed to resolve var() references when used as the second value, preventing animations from being applied. (131288246):focus-visible incorrectly matching after a programmatic focus() call triggered by clicking a button with child elements. (134337357)min-height. (134356544):has() selectors could freeze. (138431700)unicode-range. (140674753)@media (prefers-color-scheme: dark) inside an iframe did not match when the iframe’s color-scheme was set to dark. (142072593)background-clip: text did not work on table header elements. (142812484)width: 0 did not collapse a table cell to its minimum size. (142814603):has(:empty) continued to match after the targeted element’s content was dynamically changed to no longer be empty. (143864358)text-transform on elements with ::first-letter styling. (145550507)height: max-content resolved to zero on absolutely positioned elements when a child had max-height: 100%. (147333178)inline-flex container with flex-direction: column did not update its width to match the intrinsic size of a child image when the image was not cached. (150260401)zoom interacting incorrectly with font-weight, font-style, and font-variant on iPad. (152173269)aspect-ratio enforced the automatic minimum size even when min-width was explicitly set to 0. (156837730)aspect-ratio not being preserved when width: 100% and height: 100% are set but no ancestor has a defined width. (162373271):has() style invalidation performance for selectors where :has() is in non-subject position. (163512170)display: contents. (164414720)border-width value types. (168240347)inset box-shadow was incorrectly positioned on table cells with collapsed borders. (169254286)position-try-order to interpret logical axis values using the containing block’s writing mode instead of the element’s own writing mode. (169501069)fit-content, min-content, max-content) incorrectly resolved against the containing block’s height instead of being treated as auto. (171179193)@scope styles did not apply to slotted elements in web components. (171383788)nowrap minimum width calculation quirk was applied outside of quirks mode. (171410252)display property transitions caused popovers and <dialog> elements to animate incorrectly when closing. (171454696)contain: layout caused significantly slower forced layouts when all siblings created their own formatting context. (171545381)::first-letter styling. (171649994)color-scheme did not repaint iframe background. (171658244)position: absolute. (171735933)color: initial resolved to the wrong color when the system is in dark mode. (172320282)display: contents did not establish an anchor scope when using anchor-scope. (172355302)<general-enclosed> in media queries to reject content with unmatched close brackets per the <any-value> grammar. (172575115)rlh unit was double-zoomed with evaluation-time CSS zoom. (172798163)outline: auto to correctly respect zoom. (173068660)outline-offset to work correctly with outline: auto on iOS. (173130230):active, :focus-within, and :hover pseudo-classes to correctly account for elements in the top layer. (173145294)ic length unit was incorrectly affected by page scaling. (173198587)shape() function to omit default control point anchors in computed value serialization per the CSS Shapes specification. (173233716):focus-visible instead of :focus. (173321368)lh and rlh units resolved with double-zoom when line-height was a number value. (173448638)outline-width to be ignored when outline-style is auto, matching the specification. (173567890):in-range and :out-of-range pseudo-classes for time inputs with reversed ranges. (173589851):placeholder-shown to correctly match input elements that have an empty placeholder attribute. (173604635)font-size: 0. (173840866)getComputedStyle() to be zero, if the element uses position-area or anchor-center. (173885561)position-area not being able to anchor to an element positioned using anchor functions. (173964030):in-range and :out-of-range pseudo-classes to correctly update when the readonly attribute changes. (173978657)view-timeline-inset serialization failed to coalesce identical values. (174096313)url() token serialization in CSS custom properties. (174144616)text-autospace to correctly handle supplementary Unicode characters. (174148315)order values caused incorrect baseline alignment. (174241817)::first-letter text showed a pointer cursor instead of the expected I-beam cursor. (174258447)display: grid on a <fieldset> element added extra unnecessary space below its content. (174301311)outline-style. (174328839)aspect-ratio was not honored when the page was zoomed in. (174361289)height: 100% on a child element altered the layout when the parent’s height was defined via aspect-ratio. (174448267)document.styleSheets and shadowRoot.styleSheets incorrectly included adopted style sheets, which per the CSSOM specification should only appear in the final CSS style sheets list used for style resolution. (174583340)-webkit-box-pack to account for -webkit-box-direction and to handle overflow repositioning correctly. (174588996)::selection and ::highlight to disallow vendor-prefixed properties, aligning with the CSS Pseudo-Elements specification. (174590593)FontFace.loaded to reject when a local() font source fails to load. (174631384)word-break: break-all incorrectly allowed CJK close punctuation to appear at the start of a line. (174656971)word-break: keep-all incorrectly suppressed line break opportunities at CJK punctuation characters. (174658701)FontFace constructor to reject with a SyntaxError instead of a NetworkError when a BufferSource fails to parse, per the CSS Font Loading specification. (174669738)FontFace family attribute to return the serialization of the parsed value. (174698351)calc() values for the specified size suggestion. (174863227):has() sibling invalidation issues related to relation forwarding. (175006235)min-width: auto was not correctly computed for flex items. (175157619)margin-trim: block-start did not apply to blocks nested inside inline boxes. (175162899)display: contents on a <fieldset> legend caused incorrect rendering. (175163337):has() invalidation performance by including the full selector context in invalidation selectors. (175177078):hover state to repaint correctly on the customizable <select> element. (175273152)@import URLs against the <base> element URL. (175305190)-webkit-box flex distribution for children with orthogonal writing modes. (175323734)calc(infinity) as a flex-grow factor not stretching a flex item to 100% width. (175431146):has() sibling invalidation failing due to an internal bitfield overflow, causing stale styles when siblings are added or removed. (175433733):has() invalidation for sibling combinators when elements are inserted or removed from the DOM. (175441568)transition-property not preserving the specified case of <custom-ident> values during serialization. (175467206)will-change property not serializing correctly when used with non-property identifiers or identifiers in a non-standard case. (175482352)top and bottom values on relatively positioned elements not resolving when the containing block has aspect-ratio. (175502356)<select> element to use self- keywords for anchor positioning. (175505107)text-indent computation when tab stop positions are involved. (175529961)calc() margin computations in flex layout. (175532405)calc() margin computations for block, fieldset, and table caption layouts. (175548980)<li> value attributes in reversed ordered lists. (175558324)sibling-index() and sibling-count() inside calc() functions to be correctly simplified. (175590806)sibling-index() and sibling-count()to correctly return 0 when used in cross-tree ::part() styling. (175592607)resize handle not working on an element when the handle overlaps a child iframe. (175621855)calc() margins or padding lost the fixed component during intrinsic width computation. (175669222)margin-start incorrectly overlapping adjacent floats. (175669464)aspect-ratio calculations for block-level elements with size constraints. (175669713)aspect-ratio calculations for flex items with percentage cross-size constraints. (175669774)aspect-ratio calculations for flex items with definite cross-size values. (175690028)revert-layer computing incorrectly when there is a leading empty or space substitution value. (175729680):has() invalidation incorrectly resetting sibling relation bits, causing style invalidation failures for first-in-sibling-chain elements. (175738008)overflow: hidden clipping content on subsequent items. (175877530)CSSStyleDeclaration.setProperty() failing to apply !important priority to an existing inline style property when the value was an integer of 255 or lower. (176099619)min-height: min-content were incorrectly treated as scrollable, zeroing out their minimum size. (176173688):has() invalidation performance when used inside nested :is() selectors. (176354723)sibling-count() & sibling-index() used in @keyframes to re-resolve when siblings change. (176531901):has() style invalidation failing in complex nested cases involving :is(). (176719780)-webkit-perspective not establishing a containing block for fixed-positioned descendants. (176729670):has() selector performance by using scope selectors to limit style invalidation traversal for class, attribute, and pseudo-class changes. (176771971)aspect-ratio and a percentage max-width collapsing to zero width during intrinsic sizing. (176873776)max-width on elements with aspect-ratio resolving against the wrong axis in perpendicular writing modes. (176879597)z-index not applying to statically-positioned display: -webkit-box items to align with Firefox and Chrome behavior. (176886461)img with max-width and surrounding elements that caused the parent’s layout to compute incorrectly. (176889859)box-sizing: border-box providing the wrong cross size to stretched flex items. (176989934)aspect-ratio-derived height not providing a definite cross size to their flex items. (177085129)<div> positioning to fix the broken layout of paragraph spacing on some sites. (177139092)zoom to be animatable by the computed value. (177411607)attr() to align with disallowing the <url> type. (177540489)::first-letter to use the correct definition of punctuation. (177599506)offset-path to respect <coord-box> when blending shape() and basic-shape paths. (177685457)aspect-ratio providing a definite cross size to flex items when it should not. (177705930)inline-block baseline to fall back to the bottom margin edge when the content has no in-flow line boxes. (177753094):focus-visible. (177850766)@font-face font-style to serialize ‘oblique 0deg’ as ‘normal’. (178185291)font-variant longhands set after a system font. (178251443)CSSFontFeatureValuesRule.fontFamily to be settable rather than readonly. (178323504)font-style: oblique to be clamped against the font’s slant range rather than the @font-face weight range. (178324521)font-style: oblique angle being applied to the variable font ‘slnt’ axis with the wrong sign. (178326843)background and mask coordinated property list resolved values to match the specification. (178378309)none. (178476769)@font-face font-weight, font-width, and font-style oblique descriptor ranges with equal bounds to collapse to a single value, per CSSOM. (178517226)font-synthesis to avoid synthesizing styles outside of a font’s variable axis range. (178550149)font-style: italic to slant a variable font whose @font-face uses an oblique angle. (178566326)font-synthesis incorrectly applied synthetic oblique to variable fonts declared with @font-face. (178698772)height: max-content uses the used width rather than the default width. (178712792)color-mix() to allow percentages that sum to zero. (178758710)color-mix() resolution for the new 0% rules. (178921722)min-width: min-content being clamped to a smaller max-width. (178777567):last-child and related selectors incorrectly gating on parser state outside of style resolution. (178879939)@font-face font-weight descriptor explicitly restricts it to normal. (179001275)color-scheme of an <iframe> not invalidating the appearance of the embedded document. (179177141)min(), max(), clamp(), and mod(). (179534440)hsl() and hwb() colors that contain at least one none component value so they preserve their hsl()/hwb() function rather than converting to rgb(). (179854247):target) snap area over other aligned snap targets. (180108825)input[type=hidden] was not set to display: none !important in the user-agent stylesheet. (180137214)@import rules that follow an @layer statement rule. (180170656)CSSViewTransitionRule. (180170814)MediaList.deleteMedium() to parse its argument as a media query and remove all matching queries. (180270019)MediaList.appendMedium() to parse its argument as a single media query and suppress duplicates. (180291283)stretch or fit-content preferred sizes computing incorrect minimum-content contributions when sizing tracks. (180748205)var() to only resolve its fallback when the first argument resolves to the guaranteed-invalid value. (181114298)pagereveal and starting an outbound cross-document view transition. (181191512)anchor-center in vertical writing modes not being scroll-adjusted along the block axis. (181413103)origin-clean flag when reset. (177858398)fillText with textAlign=center misplacing complex-shaped text. (178682402)::first-letter were not selectable. (5688237)<font> size would be incorrectly substituted with the legacy size, causing inconsistent rendering across different default font size configurations. (15292320)execCommand('FormatBlock') did not preserve inline styles of replaced block elements, causing text formatting to be lost when pasting content. (157657531)text-indent flickered or was ignored on contenteditable elements while typing. (170280101)user-select: none. (170475401)contentEditable element to a non-editable target. (171221909)margin-top on a <legend> element inside a <fieldset> did not shift the fieldset down. (141267953)<datalist> suggestions appearing with white text on a white background in dark mode after typing. (168676757)<input> element associated with a <datalist> was intercepted by type-to-select behavior. (173346270)<input type="checkbox" switch> control behave more like other controls with regards to native appearance CSS properties. (173487610)<select multiple> did not always fire onchange when the mouse button was released far outside the element. (173882861)<select> control rendering was broken in vertical writing mode. (174068353)<select> elements with thousands of <option> children via innerHTML caused O(n²) overhead due to repeated list recalculation. (174244946)min or max attributes incorrectly matched the :in-range pseudo-class. (174829899)<input> and <textarea> elements did not preserve their user-modified state. (174892989)<option> elements to correctly implement the HTML specification’s dirtiness concept for tracking user-modified selected state. (175306111)<select> element is anchor positioned. (175454476)display value for <optgroup> and <option> elements to block, matching the behavior of other browsers. (175473184)field-sizing: content clipping the placeholder on number inputs that have no value. (175883299)<option> and <optgroup> elements to match the :disabled pseudo-class when inside a disabled <select>. (176559708)top: 100%) being mispositioned when its containing block is out-of-flow with percentage height. (177181803)<select> with appearance: base-select, including spacing, borders, border-radius, overflow, cursors, optgroup styling, picker dialog shadow, and increased contrast colors. (183345556)map element without a name attribute did not match its associated image using the id attribute. (12359382)<meta> parsing to correctly treat form feed as ASCII whitespace per the HTML specification. (108440799)javascript: URLs to align with the specification. (147612682)<img> elements with a src attribute to be dramatically slower than other browsers. (166201075)<iframe> using the srcdoc attribute did not render. (167917471)<body>, <iframe>, and <frame> elements. (171240848)replaceWith() stopped processing remaining nodes if a script in the replacement removed a sibling. (172753019)disabled attribute. (173378582)rel attribute on an <a> element multiple times did not clear prior link relations. (173567839)<li> elements. (173983892)<object> elements, aligning with other browser engines. (174537345)window.open() to correctly consume user activation when creating a new browsing context, aligning with the HTML specification. (174587258)<img sizes="auto"> to fully align with the specification. (174684058)requestClose() incorrectly firing multiple cancel events and causing a stack overflow. (174850509)requestClose() incorrectly removing the open attribute when called on a disconnected dialog element. (174855725)dir=auto on <slot> elements did not update when slotted content changed. (174871706)<option> elements rendered incorrectly when the label attribute was empty. (174979446)<source> elements with an empty type attribute inside <picture>. (175094037)innerText to emit a newline for empty <option> or <optgroup> inside <select>. (175245381)+ sign. (175300431)innerText to no longer emit newlines for visibility: hidden block elements. (175569426)innerText to correctly emit blank lines around <p> elements regardless of their CSS display value. (175729427)<a rel="ar"> elements wrapping <model> elements to correctly enter ARQL without extra steps and to display the AR badge. (176410897)innerText on tables to no longer emit spurious trailing newlines and to preserve row-exit newlines after empty rows. (176635985)<image> tags. (176712749)pushState with custom application URL schemes. (177547157)outerHTML setter to align with the HTML standard. (177788638)xmlns="" inheritance and annotation-xml encoding. (177808494)createHTMLDocument() to no longer leave the body in a parsing state. (178440940)<link rel=preload as=json> incorrectly triggered a preload. (179843455)align="center" attribute was not treated as identical to align="middle" per spec. (180128710)srcset attribute into a dynamically created iframe resulted in an invisible image. (66849050)naturalWidth and naturalHeight returning incorrect values for SVG images without intrinsic dimensions. (141196049)img element did not update its image data. (172856773)<picture> <source> candidates being speculatively preloaded even when the inner <img> has loading=lazy. (177833110)HTMLImageElement.decode() to no longer resolve spuriously after adoption, src change, or cached-image reuse. (178118012)%TypedArray%.prototype.subarray to calculate beginByteOffset correctly to align with ECMA-262. (168143600)RegExp.prototype[Symbol.split] to align with ECMA-262. (168288878)Array.prototype.concat to correctly handle arrays with indexed accessors, preventing getter reentry from bypassing Symbol.isConcatSpreadable checks. (172237596)[[Set]] to check the receiver before writing to the typed array. (173386404)%ArrayIteratorPrototype%.next() to return { done: true } instead of throwing a TypeError when the source TypedArray is detached and the iterator has already completed. (173759106)import { “``" as x } was incorrectly treated as a namespace import instead of a named import using the string “" as a ModuleExportName. (174314099)RegExp.prototype.exec and RegExp.prototype.test could match against a stale pattern if lastIndex has a valueOf that calls RegExp.prototype.compile. (174461752)Intl.Segmenter with granularity: "word" incorrectly reported isWordLike: false for numeric segments. (175057894)Object.defineProperties to call Proxy traps in the correct order. (175068687)Intl.Locale did not canonicalize before overriding the language. (175092327)Array ToPrimitive fast path incorrectly ignoring overrides of Object.prototype.valueOf. (175122250)Intl.DateTimeFormat to preserve the original legacy timezone identifier instead of replacing it with the primary IANA ID. (175206605)Promise.prototype.finally to throw a TypeError when @@species is not a constructor, matching the behavior of other browsers. (175290627)/v flag. (175559808)TypedArray.prototype.lastIndexOf by adding SIMD-accelerated reverse search for numeric types. (175904377)export statements was significantly slower than necessary. (175949532)DataView constructor to match specification-defined argument validation order and error throwing behavior. (176110210)Array.prototype.concat could produce incorrect results when combining arrays with incompatible indexing types. (176219964)TypedArray constructor edge cases involving buffer sequences to align with the specification. (176724918)WebAssembly.Memory and WebAssembly.Module to align their cloning and transferring behavior with SharedArrayBuffer. (176792374)Array.prototype.join to include prototype elements added during element toString invocation. (178055452)Temporal.Instant operations were not aligned with the spec’s abstract operations. (179844859)<mo> element attributes did not trigger a relayout. (170907029)minsize and maxsize defaults and percentages did not use the unstretched size as specified. (170908253)<mprescripts> element within <mmultiscripts> layout. (170909975)<none> and <mprescripts> elements were not laid out as <mrow> elements in MathML. (170940035)-webkit-text-fill-color when painting math variant glyphs. (172020318)padding and border rendering on <msqrt> and <mroot> elements and corrected token sizing for mathvariant. (173081436)tabIndex values not being set correctly for MathML elements. (174734133)+, −, ±, ∓, ∇, and infix operator ⋉ in the MathML Core operator dictionary. (176652211)∂ prefix operator to use the correct spacing values (3, 0) instead of (2, 1). (176693587)MathOperator being invisible when their glyph only exists in a fallback font. (178096170)<audio> and <video> controls rendering incorrectly when rotated via CSS transform. (37516619)preservesPitch and playbackRate were not correctly handled on an HTMLMediaElement connected to an AudioContext via createMediaElementSource. (93275149)MediaCapabilities.decodingInfo() incorrectly reporting VP8 in WebM as not supported. (127339546)VideoDecoder API output frames in an incorrect order for videos containing B-frames. (145093697)<source> element does not match the actual content type served by the server. (166181001)HTMLMediaElement.currentTime to report smoothly progressing values instead of updating only at fixed intervals. (170115677)decodeAudioData. (170196423)VideoFrame constructor did not handle the video color range correctly for NV12 (I420 BT601) video frames. (170299037)MediaSource. (171210968)currentTime getter to return defaultPlaybackStartPosition when no media player exists. (171722368)HTMLMediaElement to fire a timeupdate event when resetting the playback position during media load as required by the specification. (171785463)preload attribute was not properly updated when the autoplay attribute was set. (171883159)HTMLMediaElement.volume had no effect when the element was connected to an AudioContext. (174278899)ImageCapture to correctly queue takePhoto() and applyConstraints() requests to avoid concurrent capture session reconfiguration. (174950018)VTTCue text content. (175084171)<audio controls> to not show the “Subtitles” option when no subtitle track is present. (175357130)::cue() selectors to correctly match the WebVTT root object in addition to child nodes. (175550173)currentTime on iOS to update more frequently during media playback. (175774587)readyState not being updated immediately when playback stalls due to a gap in buffered data. (176330683)timeupdate events being fired during seeking before the seek operation completes. (176861767)ended event not always firing when the MediaSource duration is changed to match the current playback position. (176863546)currentTime() returning a stale value after the playback rate was changed from zero to a non-zero value. (177046564)SourceBuffer.remove() to no longer remove an extra sample, and fixed buffered ranges to cover the correct ranges. (177065364)keystatuses event when all keys have expired. (177939767)getSupportedCapabilitiesForAudioVideoType (EME) to no longer include unsupported capabilities. (178142768)MediaSession.setActionHandler to not throw an exception when called. (178167294)AudioData.copyTo to throw RangeError when frameOffset equals numberOfFrames. (178609688)PannerNode to no longer produce non-finite samples for edge-case distance parameters. (178784571)bitDepth based on transferCharacteristics. (179210193)MediaSource text-track removal loop always processed only the last track. (179508398)isValidVideoFrameBufferInit() tested displayWidth and displayHeight presence against themselves instead of the correct properties. (179514279)MediaMetadata artwork sizes parsing read the wrong substring for the height value. (179523057)pictureInPictureElement getter inverted the shadow-host connectivity check. (179675087)VideoFrame with a visibleRect rendering with offset chroma channels. (180202939)entityTransform on a <model> element while the model is unloaded or hidden. (179114750)<model> elements losing gesture interactivity after the model player is reloaded (for example, when the model scrolls out of and back into the viewport). (179249565)<meta http-equiv="refresh"> to a URL differing only in fragment identifier being incorrectly treated as a page reload. (176933795)data: URLs to be blocked for subresources such as images and scripts, aligning with the Fetch specification. (74165956)XMLHttpRequest incorrectly dropping the request body during redirects. (98459882)X-Frame-Options to only strip tab or space characters, not vertical tabs. (126915315)iso-8859-2, windows-1250, and gbk. (169566553)WKHTTPCookieStore. (174557252)sendBeacon() and the Media Session API. (177330315)xn-- that did not pass strict IDNA 2008 validation, aligning behavior with the WHATWG URL Standard. xn-- is the prefix of a punycode-encoded non-ASCII domain. (177686282)Content-* headers from 304 responses were not used to update cached entries. (179864251)Cache-Control request directives max-age, min-fresh, and no-store were not honored. (179865576)Cache-Control: public was not honored on responses with unknown status codes. (179870099)%2F following a percent-encoded Armenian path segment. (180067095)WKWebView‘s PDF export API. (180631575)NSPrintOperation dropped all text. (174756900)rowspan values exceeding the actual number of rows were incorrectly computing heights. (3209126)filter: drop-shadow() not repainting the area outside the element’s boundaries. (49387957)::first-letter styles caused Range.getClientRects() and Range.getBoundingClientRect() to return incorrect dimensions. (71546397)<td> element has an explicit height set. (78549188)margin-start could overlap an adjacent float. (93187697)position: relative on table rows (<tr>) to correctly establish a containing block for absolutely positioned descendants. (94294819)<marquee> elements causing incorrect table width calculations. (99826593)height: 100% in auto-height containers incorrectly resolved to zero height. (161699543)padding-left plus margin-left equals zero. (162376969)visibility: collapse on columns. (168556786)text-overflow: ellipsis and an inline-block pseudo-element. (168875614)min-width: fit-content rendered at an incorrect width. (169359566)height: 100% was incorrectly calculated for replaced elements like images serving as grid items nested inside a flexbox. (169431440)user-select: none. (170477571)inline-block elements so that when overflow is not visible, the baseline is correctly set to the bottom margin edge. (170575015)min-height and min-width constraints in certain configurations. (170765025)max-width on table cells when distributing width between them. (171459245)border-spacing incorrectly included collapsed columns in auto table layout calculations. (171468102)@prefers-color-scheme, it does not follow the color-scheme set by grandparents of the iframe. (172229372)about:blank iframes did not always have a transparent background. (172400258)text-wrap: balance not being applied to content with -webkit-line-clamp. (172715503)<legend> to mask the <fieldset>‘s border correctly when it has a negative left margin. (174185071)<br> elements with line-height: 0 still created extra vertical space, failing to respect the declared line height. (174400946)image-orientation being ignored for background-image, border-image, and list-style-image. (174894122)white-space: pre-wrap layout issue with justified text. (174937310)min-height: min-content inside a column flex container not shrinking to preserve its aspect ratio. (174999995)flex-wrap and flex factor computation for wrapping flex items. (175012395)writing-mode content incorrectly wrapping when the parent has auto height. (175123356)column-wrap flex container with flex-basis: 0 wrapping items into extra columns instead of stacking them when nested inside another column flex container. (175195518)filter: blur() ignored border-radius overflow clipping from its parent. (175519148)max-height was removed. (175799547)display: flex. (175866046)min-height and shrinking below their content height. (175883577)drop-shadow filters and transform: translate() incorrectly clipping nested elements after a regression. (175905543)max-height constraints. (175932457)scrollbar-gutter placement on the root element in RTL layouts. (175939512)aspect-ratio and content-box padding computed the wrong height in a column flex container. (176033726)align-content: center. (176173122)stretch. (176398251)<html> element not being repainted when the <body> background changed. (177975964)<sup> elements. (179537119)<sup> element was offset by one device pixel from the rest of the line on subpixel displays. (179586525)ex unit in text-box-edge misplaced the propagated underline, causing it to be painted twice. (179769451)min-width was not honored over max-width when sizing a shrink-to-fit container around a replaced element. (179935558)<img> embedding an SVG with a near-integral intrinsic width rendered one device pixel narrower than expected. (180490343)filter: drop-shadow() not being fully repainted when a child is resized. (181284741)text-decoration to elements with display: contents. (85691104)<text> with textLength scaling each glyph separately when x or y is a list. (94161279)href or xlink:href on SVG <image> elements had no visual effect. (96316808)width and height properties on SVG <rect>, <image>, <svg>, and other elements. (96320059)<use> and the opacity was adjusted. (96837306)attributeName was dynamically changed. (97097883)box-shadow was not drawn on fixed-positioned SVG elements. (97098951):visited link color to properly propagate to SVG through currentColor. (98776770)url(#id) was not invalidated when the filter content changed. (101870430)vector-effect to apply a transform to path geometry rather than to stroke geometry. (103573160)stroke-dashoffset values rendering with incorrect offsets when stroke-dasharray has an odd number of values. (103596361)<animateMotion> non-path animations to apply the rotate attribute. (110915794)SVGTransformList to properly allow attribute removal. (117840533)to, from, and by values such as those with leading whitespace. (118537155)<textPath>. (120284006)<img> tag did not animate correctly due to repaint artifacts with object-fit. (141815698)display: contents being visually hidden. (141825746)<tspan> positioning bug with xml:space="preserve" that caused multi-line text to render incorrectly. (143722975)SVGPathElement.getTotalLength() and . SVGPathElement.getTotalLength.getPointAtLength() to respect the CSS d property. (167195297)offsetX and offsetY for SVG elements to use the outermost SVG as the base for coordinate calculation. (168548585)fx and fy attributes on SVGRadialGradientElement to 50% to align with the SVG2 specification. (169645572)SVGAnimatedRect.baseVal to ignore invalid values set on the viewBox attribute, such as negative width or height, aligning with Firefox and Chrome. (170214971)getScreenCTM() did not include CSS transforms and zoom contributions in the legacy SVG rendering path. (171525696)em and percentage values. (171587382)SVGLength.convertToSpecifiedUnits() failing when converting from px to %, em, or ex. (172056830)<image> element was not repainted when the href attribute was removed. (172530834)<svg> root element. (172909441)parseClockValue did not reject out-of-range minutes and seconds values per the SMIL timing specification. (173577212)values attribute to preserve empty values and handle trailing semicolons. (173594455)repeat(n) event conditions not triggering animations. (173599629)getStartPositionOfChar and getEndPositionOfChar to be more compliant with the specification. (174145885)max-content and min-content use the viewBox aspect ratio when intrinsic sizes are missing. (174568894)glyph-orientation-vertical: auto to use UTR#50 Vertical Orientation properties for correct character orientation in vertical text. (175064567)preserveAspectRatio is set to none. (175173375)background-size. (175345107)glyph-orientation-vertical: auto to decode surrogate pairs for UTR#50 lookup. (175570881)stroke-dasharray interpolation to use least common multiple for list length matching and corrected composition behavior. (175598175)cx, cy, r, rx, ry, x, y, width, and height being incorrectly applied to elements such as <g> on which they are not permitted. (175672111)pathLength="0" and negative pathLength for stroke dashing. (175928827)@prefers-color-scheme in an SVG image will sometimes not follow the system color appearance. (176413340)getScreenCTM() returning an incorrect matrix when the document is scrolled under a CSS-transformed ancestor. (176814876)clip-path with nested objectBoundingBox <clipPath> to use the correct reference box. (177605894)IntersectionObserver not computing intersections for SVG element roots. (177807041)feGaussianBlur not applying when stdDeviation contains a 0 in the second component. (177906905)<text> to honor the CSS text-orientation property instead of only the deprecated glyph-orientation-vertical presentation attribute. (178044217)rotate attribute was discarded on a <textPath>, so it now composes with the path tangent angle. (178044478)getRotationOfChar() returned approximately 360° instead of 0° for full-turn rotations after normalisation into the [0°, 360°) range. (178044934)<tspan> boundary shifted the x and y value lists by one position. (178360036)d property to treat them as non-zero booleans. (178950624)pathFromEllipseElement to honor auto values for rx and ry, so APIs such as getTotalLength() return the correct length for ellipses. (178959205)<use> element. (179414226)getBoundingClientRect() on an SVG <tspan> element returned the bounds of the entire <text> element instead of the <tspan>‘s own area. (179626476)clip-path on a <clipPath> element ignored css zoom. (180162723)orient and markerUnits on <marker> not repainting elements that reference it. (181106538)number, integer-optional-integer, number-optional-number, and path animations to not apply when their from, to, or by values fail to parse. (181308150)scrollTo during a momentum scroll incorrectly interrupted the scroll, ensuring that momentum scrolling continues as expected and smooth scrolling behaves properly. (41949531)scroll-snap-type: mandatory failed after the browser chrome was hidden. (100727098)scrollIntoView with nearest alignment incorrectly aligned to the far edge for an oversized target positioned before the scrollport. (106356373)passive: false wheel event listener combined with overscroll-behavior: contain preventing scrolling. (137757208)scroll-padding did not scroll the focused element into view. (147513379)window.scrollTo() was called synchronously with a DOM layout change. (173197381)scrollIntoView() on a scrollable element incorrectly scrolled the element’s own contents. (174173683)scrollTo() call to a different target fired the scrollend event at the wrong position. (179551854)scroll-padding while scroll anchoring is active. (183145868)'self' did not match script sources in opaque-origin HTTP(S) documents. (178638597)<object> elements that load images being incorrectly blocked by the img-src Content Security Policy directive. (178772677)script-src to JSON module imports instead of connect-src. (180006320)frame-ancestors violations in report-only policies being ignored instead of reported. (180447621)nonce-source and hash-source values. (180903857)trusted-types expressions to reject trailing characters after keywords and the wildcard. (180973793)<model> elements displaying at 100x the expected size for assets authored in tools that use centimeter units. (167805672)getViewport() was called. (168125694)<model> element stagemode orbit physics behaved differently between iOS and visionOS. (172189776)XRProjectionLayer to return correct values for width, height, and layer count. (178444052)<model> element led to undesirable lighting effects. (179522538)onupgradeneeded event. (176195526)history.state being set to null when history.pushState is called from a child iframe. (50019069)activeElement. (92367314)change event was not fired on <input> and <textarea> elements when they lost focus while another application was in the foreground. (98526540)SharedArrayBuffer where [AllowShared] is not specified. (107786134)MouseEvent.offsetX and MouseEvent.offsetY were not relative to the padding edge as specified. (125763807)gamepadconnected event did not fire unless gamepad permission had already been granted. (141623162)CSPViolationReportBody did not include the source line number in Content Security Policy violation reports. (152607402)IntersectionObserver to no longer notify targets in detached documents. (162699098)toolbar.visible, statusbar.visible, menubar.visible) to return static values per the HTML specification for privacy and interoperability. (166554327)DigitalCredential protocols by gracefully filtering them out and showing a console warning instead of throwing an error. (166673454)DigitalCredentialRequest to DigitalCredentialGetRequest per the latest specification. (167115220)layerX and layerY to return correct values with CSS transforms. (168968832)location.ancestorOrigins returning stale origins after an iframe is removed from the document. (169097730)NavigateEvent.canIntercept to correctly return false when navigating to a URL with a different port, aligning with the Navigation API specification. (169845691)NavigateEvent.navigationType to return "replace" when navigating to a URL that matches the active document’s URL. (169999046)dragend event had incorrect coordinates when dragging within a nested <iframe>. (170750013)navigation.currentEntry.key did not change in private browsing windows after calling history.pushState(). (171147417)ResizeObserver callbacks became increasingly sluggish over time. (172718139)IntersectionObserver became sluggish over time when observing many elements due to O(n²) iteration. (172727210)navigation.currentEntry.id did not change in private browsing windows after calling history.replaceState(). (172897962)document.open() incorrectly aliased the caller’s security origin. (173369038)history.replaceState() on a traversed history entry incorrectly changed navigation.currentEntry.key to a new UUID instead of preserving the original key. (173388766)Object.prototype could not be serialized by structuredClone(). (173728983)once and passive flags were not preserved when copying listeners between elements. (173834642)get() request. (173918198)event.target was not set after dispatching an event in a shadow tree with no listeners. (174136382)navigator.credentials.create() and navigator.credentials.get() discarded the AbortSignal reason and always rejected with a generic AbortError. (174220589)Range.extractContents() to not extract out-of-bounds nodes when the end container is removed during extraction. (174307275)OperationError for platform-cancellation and unknown errors instead of AbortError or UnknownError. (174308268)document.createEvent() to throw an exception for "MutationEvents", "MutationEvent", "PopStateEvent", and "WheelEvent", aligning with other browser engines. (174339775)ParentNode.append() to correctly de-duplicate nodes when the same node is passed multiple times. (174365465)MutationObserver delivered childList records in the wrong order when script ran during node insertion. (174368989)URL object’s port property to whitespace behaved incorrectly. (174484035)return in the Navigation API’s performTraversal that caused incorrect behavior when traversing to an unknown key. (174513305)Blob.slice() to correctly clamp fractional start and end parameters using round-half-to-even rounding per the File API specification, which may change how edge-case fractional values like 0.5 are rounded. (174555334)postMessage() to validate transferable object states after serialization, aligning with the HTML specification. (174558047)structuredClone() and window.postMessage() to correctly throw a DataCloneError when serializing a SharedArrayBuffer outside of cross-origin isolated contexts. (174562553)Element.blur() on an <iframe> did not reset document.activeElement to <body>. (174591529)document.styleSheets to be accessible on documents created by DOMParser. (174625774)innerText getter to correctly handle trailing newlines and blank lines for <p> elements and headings. (174642704)innerText whitespace handling at inline-block boundaries. (174713114)XMLSerializer namespace handling to correctly serialize elements with namespace prefixes. (174726401)innerText getter to preserve newlines for elements with white-space: pre-line. (174727341)innerText handling of replaced elements at block boundaries. (174816319)EventSource to be closed when window.stop() is called. (174830925)preventDefault() during a pointerdown event to correctly suppress mousedown and mouseup events on iOS. (174864309)innerText to not fall back to textContent for elements with display: contents. (174883499)innerText to preserve the contents of <option> elements inside <select>. (175006854)Element.innerText to collect option text when called directly on a <select> element. (175156630)Event object’s target property could lose its JavaScript wrapper due to premature garbage collection. (175439759)TreeWalker.currentNode could be prematurely garbage collected. (175442228)FileSystemDirectoryHandle.resolve() to return the correct path array for child entries. (175645387)PerformanceNavigationTiming.domInteractive and domContentLoadedEventEnd incorrectly returning 0 instead of the correct timestamps. (175739835)FileSystemDirectoryHandle.removeEntry() to correctly remove entries. (175745157)CryptoKey to correctly remain associated with its secure context. (176157712)SharedArrayBuffer cloning and agent cluster ID assignment. (176465817)role attribute. (176713992)Notification object. (176762955)requestAnimationFrame() not providing sub-millisecond timestamp precision in cross-origin isolated contexts. (176967366)IntersectionObserverEntry.boundingClientRect to honor CSS zoom aware getBoundingClientRect. (177250323)IntersectionObserver to report correct bounds for SVG element targets. (177260411)AbortSignal abort algorithm after a lock request settles. (178589067)Regexp token for empty regexp groups. (179452346)KeyboardEvent.getModifierState("AltGraph") and MouseEvent.getModifierState("AltGraph") always returning false. (180597374)Credential.type returning "digital-credential" instead of "digital" for digital credentials. (180618646)navigator.credentials.get() leaving the digital-credentials document picker stuck on screen. (180812397)FileReader.readAsText() ignoring the charset parameter of the Blob‘s MIME type. (180890703)DOMMatrix and IntersectionObserver correctly enforcing absolute-length unit requirements when parsing values. (181453666)PannerNode orientation-only changes not updating the directional cone gain. (181413407)XMLHttpRequest from a Safari Web Extension to no longer trigger an additional permissions request. (154866064)browser.i18n.getMessage() to correctly substitute named placeholders when they appear adjacent to non-space characters. (169146196)browser.i18n.getMessage() to correctly substitute two adjacent named placeholders. (175315700)let and const. (143140659)performance.mark() records. (145226764)style attribute values in the Elements panel resulting in truncated or malformed content. (149523483)Array, Date, EventTarget, and Worker. (157178256)WebAssembly.instantiateStreaming, preventing source-level debugging in LLDB. (174362152)console.groupCollapsed() is used. (175279759)DOM.getAttributes commands per tick in cross-origin iframes instead of issuing one command per node. (178830496)Map.prototype. (180298712)Page.searchInResources silently omitting cache-backed resources from search results. (181202027)Network.setExtraHTTPHeaders to replace previously set headers instead of accumulating them. (181282814)WebAssembly.Suspending and WebAssembly.SuspendError to be data properties instead of getter functions, aligning with other WebAssembly attributes like WebAssembly.Module. (170155726)IntegerOverflow exceptions thrown by i32.rem_s, i64.rem_s, i32.div_u, i64.div_u, i32.rem_u, and i64.rem_u when both operands are constants. (175122462)RegisterSet::normalizeWidths() lost vector-width information, causing v128 argument corruption in WebAssembly SIMD thunks. (176035764)compressedTexImage not validating whether the compressed texture format extension has been enabled. (175652171)texImage functions reporting errors with incorrect function names. (175652807)GPUDevice.onuncapturederror event handler attribute not working. (149577124)maxStorageBuffersInFragmentStage and related WebGPU limits. (160800947)GPUTexture objects instead of GPUTextureView with multisampled resolve targets in render passes. (175452924)VideoFrame was encoded in an incorrect color space when encoding to VP9. (169425608)RTCPeerConnection.addIceCandidate() did not reject when the connection was already closed. (170470988)RTCDataChannel did not check the SCTP buffered amount synchronously. (172386678)MediaStreamTrack could have incorrect settings if the source settings changed while the track was being transferred. (172657570)RTCRtpSender.setParameters did not clear parameters that were unset by the web application. (173678165)RTCPeerConnection with iceTransportPolicy: "relay" failed to gather ICE candidates. (174794660)RTCInboundRtpStreamStats.trackIdentifier to match MediaStreamTrack.id. (174938984)getDisplayMedia() starting at extremely low quality and taking up to 30 seconds to become legible for remote participants. (175425085)AudioSession to remain active while microphone capture is live. (180505014)OverconstrainedError to inherit from DOMException and expose a code attribute per the Media Capture spec. (180728516)configurationchange event being dropped when a source-side change occurred while a MediaStreamTrack was muted; the event is now deferred until unmute. (180728609)Safari 27.0 comes automatically with macOS 27 Golden Gate, iOS 27, iPadOS27 and visionOS 27. Plus, you can update to Safari 27.0 on macOS 26 Tahoe and macOS 15 Sequoia, separate from macOS.
We love hearing from you. To share your thoughts, find our web evangelists online: Jen Simmons on Bluesky / Mastodon, Saron Yitbarek on Bluesky, and Jon Davis on Bluesky / Mastodon. You can follow WebKit on LinkedIn. If you run into any issues, we welcome your feedback on Safari UI (learn more about filing Feedback), or your WebKit bug report about web technologies or Web Inspector. If you run into a website that isn’t working as expected, please file a report at webcompat.com. Filing issues really does make a difference.
The WebKit team at Igalia is happy to announce a new release series of WPE WebKit. This release has two main highlights: the new WPEPlatform API, now stable and enabled by default, and a web process compositor built on the Skia graphics library, replacing the TextureMapper-based one. Read on for the details on both, along with a summary of the other most noteworthy changes from the latest release cycle.
WPEPlatform, the API that has been in the works for the past several release cycles, takes center stage in 2.54: it is now enabled by default and its API is considered stable, so applications can build on it without expecting breaking changes in future releases. Consequently, the traditional libwpe-based API is officially deprecated: it remains available and maintained, but new code should target WPEPlatform, and existing embedders are encouraged to plan their migration. This also applies to Cog, which will not have stable releases beyond the 0.18.x series.
The best part of the new API is how much simpler it is. Under libwpe, an application had to create a view backend (usually through WPEBackend-fdo’s “exportable” backend) and drive rendering, buffer management, and input dispatch itself through its callbacks. WPEPlatform moves all of that into WebKit and the platform implementation, so migrating an application is mostly a matter of deleting code: in the common case, the application constructs its WebKitWebView without a backend, WebKit selects a suitable platform automatically, and everything built on top of the web view carries over unchanged. The platform API only surfaces when an application wants more than the defaults: pinning to a particular platform, handling raw input events, or driving the toplevel window. Each of those is a few lines against a small GObject API rather than a set of C callbacks to implement.
To help embedders make the move, WPEPlatform is now extensively documented. The reference documentation includes an overview of the platform API (covering its relationship to libwpe and what is intentionally not part of the new API), a guide on compiling against it, a tutorial on writing a browser, and a tutorial on writing a WPE platform implementation. A guide on migrating from libwpe, with a symbol-by-symbol mapping table, walks embedders through the process step by step.
Since WPEPlatform is now built by default, the wpe-platform-2.0 pkg-config module is available in regular builds. Conversely, the legacy API can be disabled at build time with ENABLE_WPE_LEGACY_API=OFF when it is not needed.
New platform APIs added this cycle include:
WPEProcessManager and WPEProcessLaunchOptions, which allow the embedder to control how the auxiliary WebKit processes are launched and terminated. This is particularly important on Android, where each process is a service that must be started with bindService(), and it removes the last reason a WPEPlatform-only build could not work there. Note that, unlike the rest of WPEPlatform, the process management API is only built when targeting Android and remains experimental: it is not yet generic enough to be enabled everywhere, and it may still change in future releases.wpe_gamepad_has_rumble() and wpe_gamepad_rumble(), with a built-in implementation based on libmanette. This enables the Gamepad API vibrationActuator for web content.WPE_SETTING_OVERLAY_SCROLLBARS setting, enabled by default, which may be disabled by applications to opt into classic, always-visible scrollbars.WPE_INPUT_PURPOSE_SEARCH input purpose, allowing input methods to detect when the values of an input field are expected to be search terms.A few final adjustments were made to the API before declaring it stable, which may require updates to platform implementations and applications developed against the earlier previews:
WPE_SETTING_DISABLE_ANIMATIONS setting has been replaced by WPE_SETTING_REDUCED_MOTION, matching the dedicated reduced-motion setting introduced in GNOME 50, and a new tri-state WPE_SETTING_INTERFACE_CONTRAST setting has been added. Together with the existing WPE_SETTING_DARK_MODE setting, these make the prefers-reduced-motion, prefers-contrast, and prefers-color-scheme media queries follow the platform settings.wpe_gesture_controller_handle_event() now returns a boolean indicating whether the event was consumed.WPE_DMABUF_BUFFER_FORMAT environment variable has been renamed to WPE_BUFFER_FORMAT.A good measure of the new API’s maturity is wpe-android, which has been rebuilt this year as a WPEPlatform platform implementation living entirely outside the WebKit tree: Android’s display system now looks to the engine like any other WPE platform, and applications get a convenience Java API modeled after android.webkit.WebView. The new WPEProcessManager API removed the last dependency on libwpe there. You can read more about it in this post.
The shared WebKit API has also seen additions this cycle:
webkit_favicon_database_get_page_icons() retrieves all the icons declared by a page as a list of WebKitImage objects, and the WebKitWebView:page-icons property exposes the icons of the currently loaded page. Each icon implements GLoadableIcon, so applications can pick the size that best fits their needs. With this, the favicon database is available in WPE for the first time; it can be enabled with webkit_website_data_manager_set_favicons_enabled().WebKitWebView:magnification property to handle visual scaling of the page, independent of the zoom level.WebKitWebsitePolicies:custom-user-agent allows setting a custom User-Agent for a single navigation, by passing the policies to webkit_policy_decision_use_with_policies().webkit_feature_list_find() is a convenience function to look up a feature by identifier in a feature list.This cycle brings the largest overhaul of the rendering architecture since the adoption of Skia for 2D rendering: the web process compositor now uses the Skia API instead of the venerable TextureMapper. Layers are composed into the final frame using Skia, which allows sharing a single rendering infrastructure across the whole graphics stack and enables several optimizations:
Beyond raw performance, expressing compositing as Skia draw calls made several features simpler and faster: filters and masks no longer require intermediate offscreen surfaces in most cases, and CSS blend modes, which TextureMapper never implemented, now work in composited layers. And since Skia can target both OpenGL and Vulkan, the compositor no longer stands in the way of Vulkan-based rendering in the future.
The new compositor also works when using the legacy libwpe-based API.
The consolidation on Skia goes beyond compositing: the option to use Cairo for 2D rendering has been removed, making Skia the only 2D rendering implementation. Rendering tiles in the main thread is no longer supported either, so threaded rendering is now the only tile painting path.
Damage tracking has seen substantial work this cycle. The damage is the region of the view that changed since the previous frame and therefore requires repainting; Paweł Lampe’s introduction to damage propagation covers the concept in depth. Compositing itself now uses this information: each draw the compositor issues is restricted to the damaged rectangles, in a way that preserves the batched painting described above, and damage propagation to the platform is now enabled by default. A new DamageRectangleThreshold preference allows embedders to tune the balance between damage precision and bookkeeping cost.
What drove the compositor rewrite was making it simpler and easier to maintain, as Carlos García Campos explained at the Web Engines Hackfest 2026; performance came later, since TextureMapper started out ahead after more than a decade of tuning. By now the optimization work has more than closed that gap. The public WPE performance dashboard, which continuously runs benchmarks on Raspberry Pi 4 devices, tells the story: comparing the last TextureMapper-based revisions against the current ones, and thus measuring the cumulative effect of the graphics work described in this section, the MotionMark score is up by around 36%. On the composition-focused variant of the benchmark, where the compositor dominates the workload, the score is up by around 45%.
The dashboard also records the GPU load during each run, and there the difference is even more telling. MotionMark increases scene complexity until the browser can no longer sustain the target frame rate, so a higher score already means more work per frame; the Skia compositor delivers that while keeping the GPU markedly less busy. On embedded devices, where the GPU is typically the scarcest resource, this headroom translates directly into smoother pages.
One part of the work goes largely unmeasured here, though. MotionMark animates nearly the entire viewport, so there is hardly anything for damage tracking to save. Real-world content behaves differently: usually a small area of the page is changing while everything else stays still, and skipping all of that quiet area cuts the per-frame GPU work to a fraction.
A GPU atlas is now used for batched raster image uploads, regardless of the compositor in use, and it is reused across frames when the image set does not change, avoiding needless texture allocation and pixel uploads.
Asynchronous scrolling is smoother: several synchronization issues between the main, scrolling, and compositing threads that caused glitches while scrolling have been fixed, and the scrolling thread no longer blocks the compositor to flush its state, removing input-latency stalls on pages with many layers.
Finally, more animations can now run on the compositing thread: CSS animations using the steps() and linear() timing functions no longer force main-thread animation.
Let’s start with the transition: the GStreamer-based WebRTC backend is being replaced with a LibWebRTC-based implementation, which is expected to be available in the next release cycle. As a consequence, WebRTC support, which in previous releases required building with experimental features enabled, is disabled in 2.54.
The rest of the multimedia work moved forward at full speed, with a strong focus on embedded hardware:
qtic2vdec and qtic2venc GStreamer elements.WEBKIT_GST_VIDEO_DECODING_LIMIT environment variable, so a single build can serve devices with different capabilities.mp4a codecs (MP3, AC-3, E-AC-3) are correctly reported as supported when decoders are present, xHE-AAC support is auto-detected, and Dolby AC-4 is advertised for MSE on systems that support it.SourceBuffer.changeType() has been added to the MSE backend, along with a fix for playback stalling at ad transitions on Twitch.On top of this, the GStreamer backend has received a substantial amount of memory-safety and lifetime-correctness work that translates into a more stable multimedia experience.
The OpenXR-based WebXR implementation continues to progress. The main highlight is support for the WebXR Layers API: quad, cylinder, equirect, and cube layers are now implemented, in addition to the already supported projection layers. Layers can be backed by texture arrays, and XRSession.maxRenderLayers lets content query the compositor’s layer budget.
The backend has also been decoupled from OpenGL ES through an abstract graphics binding, paving the way for a future Vulkan-based binding.
WebXR support remains a build-time option, enabled with the ENABLE_WEBXR=ON CMake option; the Layers support additionally requires ENABLE_WEBXR_LAYERS=ON.
Several long-standing gaps in the WPE port have been closed this cycle:
<select> elements when the WebKitWebView::show-option-menu signal is left unhandled, so option menus work out of the box.ENABLE_SPELLCHECKING CMake option.ENABLE_POINTER_LOCK CMake option.As usual, this list is not exhaustive as WebKit continuously progresses in its support for new standards. Some of the highlights for this release are:
Date, is now enabled.appearance: base-select) are now supported.closedby attribute for <dialog> are now enabled.ReadableStream.from() are now enabled.Date and Intl.WebKit can now use mimalloc as its memory allocator, as an alternative to its own bmalloc. For now it is the default only on some architectures (32-bit ARM, MIPS, RISC-V, and builds supporting 64 KB memory pages); everywhere else bmalloc remains the default, and mimalloc can be enabled with the USE_MIMALLOC build option.
Logging now falls back to the standard error output when journald is not reachable, which is common in minimal containers, and the new WEBKIT_DEBUG_OUTPUT environment variable allows choosing the log destination explicitly.
Profile-guided optimization is now supported in regular CMake builds with Clang, through the ENABLE_LLVM_PROFILE_GENERATION and USE_PGO_PROFILE options.
Finally, a note for packagers: building WPE WebKit now requires Ninja, as the CMake Makefile generator is no longer supported.
The 2.56 release series will bring even more improvements, and we expect it to be released during the spring of 2027. Until then!
Update on what happened in WebKit in the week from September 7 to September 14.
In this week's edition, the PNG image decoder received a much needed cleanup, as well as another swipe of unassorted graphics improvements, most notably in the Layer-Based SVG Engine. Finally, we also have an exciting and informative article about how to use the WPE Platform API on Raspberry Pi.
Cleaned up the PNG image decoder a little bit, by removing conditional code that was used to support libpng versions older than 1.5.0, and requiring that as the minimum version. Given that version 1.5.0 was released back in 2011, it is expected that every system where current WebKit works will have much newer versions of libpng anyway.
Fixed an assertion failure for an outermost <svg> with non-visible overflow in the Layer-Based SVG Engine (LBSE) by refreshing its scroll dimensions at the end of layout, a first step towards reworking <svg> scrolling, which is not yet spec compliant.
Fixed an assertion failure when computing filter outsets for <feGaussianBlur> and <feDropShadow> with a negative stdDeviation, which turns the primitive off but was still passed on to the outset calculation, affecting both SVG filters and CSS reference filters.
Stopped reporting damage every frame for composited layers that use CSS filter in the Skia based compositor, so a blurred or drop-shadowed element is now only repainted when its subtree, or the applied (possibly animated) filter value, actually changed.
Fixed the bug that composited layers with clipping or masking and fully covered by opaque child layers were treated as opaque layers.
Fixed two debug assertion failures in the Layer-Based SVG Engine (LBSE), one where an SVG transform change left ancestor layer repaint rects stale and one where repainting on a compositing change during layout tripped an over-strict check on the paint offset cache.
Fixed the white noised image issue on the combination of i915 driver and Intel Arc after 308458@main introduced image atlas uploading.
Published a blog post describing the steps to try WPE Platform API on Raspberry Pi with latest WebKit main branch.
That’s all for this week!
The WPE and GTK ports have supported touch events for a long time, but asynchronous scrolling only worked for wheel events. Scrolling driven by touch still depended on the web process main thread. This change puts touch events on the asynchronous scrolling path too.
Let’s start with the background: what asynchronous scrolling is and why it needs to be built differently here.
A naive implementation of scrolling looks like this:
preventDefault(), the scroll position is updated.Step 3 is the problem. The main thread is easily blocked for hundreds of milliseconds by JavaScript execution or layout, and scrolling is frozen for that whole time. Your finger moves, the screen does not. That is what synchronous scrolling feels like.
Asynchronous scrolling moves the work off the main thread: the scrolling thread updates the scroll position, and the compositor thread composites and presents the frame. Neither needs the main thread, so scrolling keeps running at 60fps even when the main thread is busy.
Two data structures make this possible.
The scrolling tree is a tree of the scrollable areas of a page. The main
frame, overflow: scroll elements, position: fixed/sticky elements and so on
each become a node holding its own scroll position and a reference to its layer.
The scrolling thread updates scroll positions by looking only at this tree, and
the compositor thread then draws the layers at their new positions — the main
thread is not involved in either step.
But there are places where scrolling on its own would be wrong, because the page
might call preventDefault() from an addEventListener("touchstart", ...)
handler. That is what event regions are for.
An event region records, per layer and at rendering time, “this rectangle has a
listener for this kind of event”. EventRegion keeps separate regions for
touchstart, touchmove, pointerdown, mousedown and friends, and for any
given point it yields a TrackingType:
enum class TrackingType : uint8_t {
NotTracking = 0, // No listener. The event does not even need to be delivered.
Asynchronous = 1, // Passive listeners only. Scroll now, notify the page later.
Synchronous = 2 // A non-passive listener may call preventDefault(). We must wait.
};
The passive distinction is what makes Asynchronous possible.
preventDefault() cancels an event only if the listener was registered with
passive: false; from a passive listener it does nothing. And on window,
document and document.body, touchstart/touchmove (and wheel) default to
passive: true — see
MDN: Using passive listeners.
So Asynchronous is the case “there are listeners, but none of them can cancel
the scroll”: start scrolling now, deliver the event to the main thread
afterwards.
Because this information travels to the scrolling thread along with the layer tree, an incoming input event can be classified without waking the main thread. That is the heart of asynchronous scrolling.
The iOS port already implements asynchronous scrolling for touch events, but it could not be reused, because the process layout is different.
iOS:
UI process: platform layer tree + scrolling tree + touch event input
Web process: main thread (DOM, layout)
WPE / GTK (Coordinated Graphics):
UI process: touch event input only
Web process: main thread (DOM, layout)
+ EventDispatcher thread / scrolling thread
+ platform layer tree + scrolling tree
On iOS both the platform layer tree and the scrolling tree live in the UI process — the very process that receives touch events — so the classification and the scroll both happen right there. On WPE and GTK we use Coordinated Graphics, and both trees live in the web process instead. The classification therefore has to happen after sending the event to the web process, but before touching the main thread.
Fortunately the same problem was already solved for wheel events. The web process has an EventDispatcher thread that receives wheel events from the UI process without going through the main thread and consults the scrolling tree directly. This change builds the same shape for touch events.
One more piece of background: in WPE a touch does not scroll the page directly. Touch events are first offered to the page; only if the page does not consume them does the UI process turn the touch sequence into scrolling.
The decision point is PageClientImpl::doneWithTouchEvent(). If the page handled
the event, gesture detection is cancelled with
wpe_gesture_controller_cancel() so the engine does not also act on it. If it
was not handled, the event is fed to the WPE platform gesture controller via
ViewPlatform::handleGesture(), and a recognized WPE_GESTURE_DRAG is turned
into a synthetic scroll event pushed back into the page as a wheel event:
GRefPtr<WPEEvent> simulatedScrollEvent = adoptGRef(wpe_event_scroll_new(
m_wpeView.get(), WPE_INPUT_SOURCE_TOUCHSCREEN, 0, static_cast<WPEModifiers>(0), dx, dy, TRUE, FALSE, x, y));
page().handleNativeWheelEvent(WebKit::NativeWebWheelEvent::create(simulatedScrollEvent.get(), phase));
That TRUE is precise_deltas: touch-driven scrolling in WPE reaches the engine
as precise-delta wheel events, which becomes relevant later.
The important consequence is this: the UI process cannot start scrolling until it knows whether the page is going to consume the touch. That answer used to come from the web process main thread — so when the main thread was busy, scrolling did not start. That is the problem this change fixes.
A new ENABLE(COORDINATED_TOUCH_EVENTS) is introduced in PlatformEnableGlib.h,
and it turns on ENABLE(TOUCH_EVENT_REGIONS) whenever touch events are enabled
on WPE/GTK. The AlwaysUseTouchEventRegions preference now defaults to true
under that flag, so Document::shouldUseTouchEventRegions() returns true and
touch regions are actually recorded on the layers during rendering.
The old WebPageProxy::handleTouchEvent() consulted a touchEventTracking state
kept in the UI process and sent Messages::WebPage::TouchEvent, i.e. straight to
the web process main thread.
The new version delegates all classification to the web process and only queues
events and delivers answers. One event is in flight at a time; the next is sent
when the reply arrives. The flood of touchmove events produced while a finger
moves is coalesced into the newest queued event when that is also a touchmove,
and the coalesced events are flushed to doneWithTouchEvent() together with the
reply.
The destination is now Messages::EventDispatcher::TouchEvent.
EventDispatcher::touchEvent() runs on the EventDispatcher thread, where it
looks up the page’s scrolling tree, asks it for a TrackingType, and splits
three ways:
handled = false immediately, without
bothering the main thread at all. The UI process can start scrolling right
away.handled = false first so scrolling starts, then deliver the event to the main thread.preventDefault(), so wait
for the main thread result as before.Replying without a main thread round trip is possible because the new
TouchEvent message is declared AnyThread in EventDispatcher.messages.in.
The iOS equivalent is MainThreadCallback, which always replies from the main
thread.
If there is no scrolling tree for the page yet, the event goes to the main thread as before.
The classification itself is
ScrollingTreeCoordinated::eventTrackingTypeForTouchEvent(). It works in two
stages.
First, for each newly pressed touch point: convert the point from view to contents coordinates,
hit test the layer tree down from the root contents layer, take the frontmost
layer whose event region contains the point, and query that region. It is queried
for many event types, because a touch fires more than the DOM touch* events —
pointer*, compatibility mouse events and gesture* too, and a non-passive
listener for any of them forces synchronous handling. The results are folded into
a small TouchEventTracking struct with four fields: start, move, end and
force-change.
Second, the tracking type of the event as a whole is derived from the touch point
states, merging the per-field values. Merging picks the stronger of two types
(NotTracking < Asynchronous < Synchronous), so if any single point needs
synchronous handling, the whole event is synchronous.
TouchEventTracking persists for the lifetime of a touch sequence and is reset
once all points are released, so the hit test done at touchstart is reused for
the following touchmove/touchend. That guarantees a sequence never flips from
synchronous to asynchronous halfway through just because a finger moved off a
listener’s area.
<input type=range> #A slider handles touches internally even with no JavaScript listener, so looking
at the event region alone would classify it as NotTracking.
HTMLInputElement::updateTouchEventHandler() now sets the
HasInternalTouchEventHandling flag on EventTarget for range inputs, and
StyleAdjuster turns that flag into the full set of touch region types for the
element.
The last piece is in ScrollingEffectsController::handleWheelEvent(). As shown
above, WPE synthesizes wheel events from touch gestures with precise deltas.
Precise-delta events only need immediateScrollBy() to move the scroll
position — but then nothing drives screen updates while the main thread is busy.
The fix is that, while a scroll gesture is in progress, a scroll animation is
also started — from one ULP short of the destination (std::nextafter()) to the
destination. Visually it finishes instantly — the
real scroll is still done by immediateScrollBy() — but a scroll animation is
now running, which starts display link monitoring and keeps compositing driven
regardless of the main thread.
Before:
After (no listeners, or passive listeners only):
Where a non-passive listener exists, we still wait for the main thread as before.
The spec requires preventDefault() to be honoured, so that is unavoidable.
As a side effect, the tests under fast/events/touch/ had to be updated.
Event regions are computed during a rendering update and propagated to the scrolling tree via the platform layer tree. Which means a test like this:
target.addEventListener("touchstart", handler);
tapSoon(20, 20); // ← the region has not been updated yet!
taps immediately after registering the listener, while the scrolling tree still
believes there is no listener and returns NotTracking. The event never reaches
the main thread and the test fails.
A new UIHelper.renderingComplete() was added for this:
static async renderingComplete()
{
// Wait for the platform layer tree to be updated
await UIHelper.animationFrame();
await UIHelper.animationFrame();
}
Two animation frames are needed because the first one runs the rendering update that computes the regions, and a second is needed for the result to reach the layer tree.
AnyThread so it can be sent without waiting for the main thread.Many thanks to Alejandro G. Castro and Carlos Garcia Campos for their insightful reviews of this work, and to Claude for writing this blog post.
Safari Technology Preview Release 252 is now available for download for macOS Golden Gate and macOS Tahoe. If you already have Safari Technology Preview installed, you can update it in System Settings under General → Software Update.
This release includes WebKit changes between: 319252@main…320112@main.
display: contents element incorrectly including text from its siblings. (319287@main) (185108892)width or height being incorrectly accelerated when only some keyframes have a size-dependent transform. (319363@main) (185153675)revert-layer or revert-rule no longer being recomputed after a KeyframeEffect is copied. (319368@main) (185164643)scroll-timeline-name and view-timeline-name to be loosely matched across shadow tree boundaries. (320040@main) (185206745)named-feature() function in @supports conditions. (319412@main) (183688843)CSSConditionRule.supports. (319302@main) (184943650)user-select CSS property. (319562@main) (184964807)CSSMediaRule.matches. (319988@main) (185154835)text-decoration-inset. (320052@main) (185850399)line-clamp not clamping content inside an inline block. (319848@main) (168534323)line-clamp is applied to content with a block inside an inline box. (319392@main) (182123153)display for -webkit-box and -webkit-inline-box when combined with -webkit-box-orient: vertical and the continue property. (319370@main) (182288734)random() and random-item() with an auto caching key in the same property value producing the same random value. (319463@main) (182846761)margin-trim being reflected in computed style. (319612@main) (184215336)justify-content. (319353@main) (184542148)::selection failing to repaint when its style changes. (319662@main) (184994906)attr() with a number or percentage value. (319994@main) (185088893)anchor(right) not resolving to an inline anchor’s right edge in vertical-rl writing mode. (319286@main) (185108663)px values inside calc() expressions being serialized incorrectly in computed style when zoom is applied. (319320@main) (185156231)text-overflow: ellipsis not being shown on content that is not clamped when using line-clamp. (319597@main) (185348784)offset-path not respecting corner-shape when using a <coord-box> reference box. (319655@main) (185447602)corner-shape drawing square and notch corners as round and scoop on outlines and box-shadow spread. (319606@main) (185452130)FontFace loaded promise. (319575@main) (185462848)lh unit. (319596@main) (185508307)corner-shape for outset curves to match the updated specification algorithm. (319661@main) (185519894)inline, inline-start, and inline-end values from margin-trim, which now accepts none | block | [ block-start || block-end ]. (319622@main) (185527437)calc() simplification so that clamp(none, a, b) becomes max(a, b) and clamp(a, b, none) becomes min(a, b). (319614@main) (185531467)align-content moving a floated child twice in block containers. (319665@main) (185536955)background-size: contain, background-size: cover, and automatic sizing ignoring SVG images that have one intrinsic dimension and no aspect ratio. (319972@main) (185970333)flow-tolerance property to fit-tolerance. (319795@main) (185125327)<summary> or <button> element also activating that element. (319628@main) (185479319)appearance: base-select to center-align items, use lh units for padding, and reset all font properties on ::picker-icon and ::checkmark. (319816@main) (184843832)<input type="number"> not repainting when the window’s active state changes. (319683@main) (185625389)show() because the <dialog> was still treated as unrendered. (319387@main) (185156091)dialog.close() restoring focus even when focus was not inside the dialog and the dialog was not modal. (319640@main) (185549474)documentElement.focus() to move focus to the viewport instead of doing nothing. (319730@main) (185567540)DOMParser executing after being cloned. (319922@main) (185943138)ImageDecoder not applying an image’s EXIF orientation to the decoded frames. (319527@main) (184850407)ImageDecoder producing VideoFrame objects with a duration of zero. (319526@main) (185257570)worker.terminate() and page reloads not terminating a worker that is blocked in Atomics.wait or a WebAssembly atomic.wait. (319508@main) (147482360)Promise.try() to resolve its result with PromiseResolve, so a promise returned by the callback is no longer wrapped in an extra promise. (319276@main) (185064567)Uint8Array.prototype.setFromBase64() so that nothing is read and no error is thrown when the target array has zero length. (319275@main) (185073630)s flag matching from the start of the string instead of from lastIndex. (320086@main) (185814135)Referer header sent for descendant and dynamically imported module script fetches. (319921@main) (185943042)String.prototype.replace with a unicode regular expression splitting a surrogate pair after an empty match. (320099@main) (186143674)MEDIA_ERR_ABORTED and restarting from the beginning after restoring a page from the back/forward cache. (319472@main) (183119720)ManagedMediaSource not firing the startstreaming event when seeking outside the buffered range. (319330@main) (183790572)ended event being fired when seeking to the end of an HTMLMediaElement backed by a MediaSource. (319847@main) (185145277)Expires date uses the month-before-day format produced by Date.prototype.toString(). (320016@main) (185840799)iframe whose owner element generates no box. (319910@main) (181639379)backdrop-filter being clipped to a rounded rectangle instead of following corner-shape. (320063@main) (185813432)max-width length was not honored on an out-of-flow replaced element with an intrinsic aspect ratio. (319917@main) (185831651)background-size: cover painting nothing for an SVG image with an extreme natural aspect ratio. (320008@main) (186006463)text-overflow as an SVG presentation attribute. (319563@main) (176146717)url() references to SVG resources such as filters, masks, and gradients in external documents. (319411@main) (183940963)use-script, no-change, and reset-size values from dominant-baseline to match SVG 2. (319256@main) (174635806)stack property on DOMException, matching other error objects. (319782@main) (185770735)navigator.credentials.get() promise synchronously instead of queuing a global task as required by the specification. (319421@main) (182678875)fetch() response body being withheld from the reader until the next network chunk arrived. (319981@main) (185883588)AudioBufferSourceNode ignoring the offset passed to start() when using a negative playbackRate. (319674@main) (184484940)AudioBufferSourceNode to clamp out-of-range loopStart and loopEnd values. (319755@main) (185752079)AudioBufferSourceNode rendered only silence when start() was called with an out-of-bounds offset and a negative playbackRate. (319779@main) (185769043)memory64 together with multiple memories. (319336@main) (185152671)CompileError messages when compiling a WebAssembly module with more than one invalid function by always reporting the error for the lowest function index. (319430@main) (140651602)WebAssembly.Tag not being the same object as the one that was imported. (319294@main) (185121644)WebAssembly.Global so that the exports object returns the same object instead of a new wrapper. (319351@main) (185184951)WebAssembly.Module.imports() and WebAssembly.Module.exports() exposing the type field from the unfinished JS Type Reflection proposal. (319427@main) (185281658)ImageBitmap. (319576@main) (184913852)getAttribLocation() and getUniformLocation() not reporting an INVALID_OPERATION error when querying an unlinked program with a reserved name prefix. (319903@main) (185565011)snorm10-10-10-2 value of GPUVertexFormat. (319603@main) (185379686)sampler_comparison type incorrectly. (320037@main) (186031593)?transport=tcp being rejected, which prevented relay ICE candidates from being gathered. (319365@main) (184556197)WPE Platform API (also known as “new API”) is a redesigned, GObject-based platform-integration layer for WPE WebKit that replaces the older libwpe backend model. A few months ago, Kate and
Simon published two closely related blog posts about it. The first one focuses more on the API and browser
implementation details, while the second one focuses more on writing and integrating the browser within Linux distribution.
This article builds on top of the above ones, and showcases how to build and try a minimal WPE browser using WPE Platform API on Raspberry Pi. Moreover, as the WPE Platform API still evolves to some degree, this article
also explains how to use and stick to the latest WPE WebKit from main branch. This way one can play with all the latest features straight on embedded hardware.
Before going further, one should be aware that in case of a simple release build (instead of one using latest main branch) it’s better to follow official instructions instead of this article.
This article focuses on a certain setup using Raspberry Pi 3B but it should be fairly easy to adapt the config to any other Raspberry Pi model.
As for the work environment: the Linux-based host with ability to run containers was used along with WebKit Container SDK. The SDK version was precisely 2.53-v6-d535e88 as it uses
Ubuntu 24.04.4 LTS that works well with Yocto scarthgap.
The Yocto scarthgap has been used to increase the chances that the config and commands demonstrated in this article will remain buildable for many years to follow.
The preparation of the image starts with a series of commands that create a main directory and clone important Yocto repositories along with some meta layer repositories. At this point already, it’s important to have the working directory shared between host and SDK.
# host
mkdir wpe-upstream
cd wpe-upstream
git clone https://git.yoctoproject.org/git/poky -b scarthgap
git clone git@github.com:openembedded/meta-openembedded.git -b scarthgap
git clone https://git.yoctoproject.org/git/meta-raspberrypi -b scarthgap
git clone https://github.com/Igalia/meta-webkit -b scarthgap
source poky/oe-init-build-env build
Once the build directory is created, it’s necessary to configure the meta layers in the build/conf/bblayers.conf file the following way:
# POKY_BBLAYERS_CONF_VERSION is increased each time build/conf/bblayers.conf
# changes incompatibly
POKY_BBLAYERS_CONF_VERSION = "2"
BBPATH = "${TOPDIR}"
BSPDIR := "${@os.path.abspath(os.path.dirname(d.getVar('FILE', True)) + '/../..')}"
BBFILES ?= ""
BBLAYERS ?= " \
${BSPDIR}/poky/meta \
${BSPDIR}/poky/meta-poky \
${BSPDIR}/poky/meta-yocto-bsp \
${BSPDIR}/meta-openembedded/meta-oe \
${BSPDIR}/meta-openembedded/meta-multimedia \
${BSPDIR}/meta-openembedded/meta-python \
${BSPDIR}/meta-raspberrypi \
${BSPDIR}/meta-webkit \
"
With the above, the recipes from the meta layers cloned earlier will be considered by bitbake.
Next, the most important configuration step is appending the following to build/conf/local.conf:
MACHINE = "raspberrypi3-64"
MACHINE_FEATURES:append = " vc4graphics"
GPU_MEM_256 = "128"
GPU_MEM_512 = "196"
GPU_MEM_1024 = "396"
DISTRO_FEATURES:append = " opengl egl wayland"
EXTRA_IMAGE_FEATURES = "debug-tweaks"
IMAGE_FEATURES:append = " ssh-server-dropbear hwcodecs"
IMAGE_INSTALL:append = " wpewebkit wpe-browser"
PREFERRED_VERSION_wpewebkit = "latest"
LICENSE_FLAGS_ACCEPTED = "synaptics-killswitch"
With that, wpewebkit latest will be preferred and installed in the image along with a dummy browser called wpe-browser.
To make the wpewebkit latest work, one needs to create meta-webkit/recipes-browser/wpewebkit/wpewebkit_latest.bb:
SUMMARY = "Lightweight WebKit port for embedded devices with OpenGL-ES acceleration"
DESCRIPTION = "WPE WebKit port pairs the WebKit engine with OpenGL-ES (OpenGL for Embedded Systems), \
allowing embedders to create simple and performant systems based on Web platform technologies. \
It is designed with hardware acceleration in mind, relying on EGL, and OpenGL ES."
HOMEPAGE = "https://wpewebkit.org/"
BUGTRACKER = "https://bugs.webkit.org/"
LICENSE = "BSD-2-Clause & LGPL-2.0-or-later"
LIC_FILES_CHKSUM = "file://Source/WebCore/LICENSE-LGPL-2.1;md5=a778a33ef338abbaf8b8a7c36b6eec80 "
REQUIRED_DISTRO_FEATURES = "opengl"
DEPENDS:append = " \
libsoup \
bison-native gperf-native harfbuzz-native libxml2-native ccache-native ninja-native ruby-native \
fontconfig freetype glib-2.0 harfbuzz icu jpeg pcre sqlite3 zlib libpng libtasn1 \
libwebp libxml2 libxslt virtual/egl virtual/libgles2 libepoxy libgcrypt \
unifdef-native \
"
inherit cmake features_check pkgconfig perlnative python3native
export WK_USE_CCACHE = "NO"
PACKAGECONFIG ??= "accessibility avif dfg-jit gbm gpu-process \
jit jpegxl libbacktrace \
mediasource mediastream \
remote-inspector \
sysprof \
${@' system-sysprof' \
if bb.utils.contains('BBFILE_COLLECTIONS', 'meta-gnome', True, False, d) \
else '' } \
unified-builds video webaudio woff2 wpe-platform \
${@bb.utils.contains('DISTRO_FEATURES', 'systemd', 'journald', '' ,d)} \
"
PACKAGECONFIG[reduce-size] = "-DCMAKE_BUILD_TYPE=MinSizeRel,-DCMAKE_BUILD_TYPE=Release,,"
PACKAGECONFIG[release-with-debug-info] = "-DCMAKE_BUILD_TYPE=RelWithDebInfo,-DCMAKE_BUILD_TYPE=Release,,"
# WPE features
PACKAGECONFIG[accessibility] = "-DUSE_ATK=ON,-DUSE_ATK=OFF,atk at-spi2-atk"
PACKAGECONFIG[avif] = "-DUSE_AVIF=ON,-DUSE_AVIF=OFF,libavif"
PACKAGECONFIG[bubblewrap] = "-DENABLE_BUBBLEWRAP_SANDBOX=ON -DBWRAP_EXECUTABLE=${bindir}/bwrap -DDBUS_PROXY_EXECUTABLE=${bindir}/xdg-dbus-proxy,-DENABLE_BUBBLEWRAP_SANDBOX=OFF,bubblewrap xdg-dbus-proxy libseccomp"
PACKAGECONFIG[developer-mode] = "-DDEVELOPER_MODE=ON,-DDEVELOPER_MODE=OFF,wayland-native wayland-protocols wpebackend-fdo"
PACKAGECONFIG[deviceorientation] = "-DENABLE_DEVICE_ORIENTATION=ON,-DENABLE_DEVICE_ORIENTATION=OFF,"
PACKAGECONFIG[dfg-jit] = "-DENABLE_DFG_JIT=ON,-DENABLE_DFG_JIT=OFF,"
PACKAGECONFIG[documentation] = "-DENABLE_DOCUMENTATION=ON,-DENABLE_DOCUMENTATION=OFF, gi-docgen-native gi-docgen"
PACKAGECONFIG[encryptedmedia] = "-DENABLE_ENCRYPTED_MEDIA=ON,-DENABLE_ENCRYPTED_MEDIA=OFF,libgcrypt"
PACKAGECONFIG[experimental-features] = "-DENABLE_EXPERIMENTAL_FEATURES=ON,-DENABLE_EXPERIMENTAL_FEATURES=OFF,libavif libjxl"
PACKAGECONFIG[gamepad] = "-DENABLE_GAMEPAD=ON,-DENABLE_GAMEPAD=OFF,libmanette"
PACKAGECONFIG[gbm] = "-DUSE_GBM=ON,-DUSE_GBM=OFF,libdrm"
PACKAGECONFIG[geolocation] = "-DENABLE_GEOLOCATION=ON,-DENABLE_GEOLOCATION=OFF,geoclue"
PACKAGECONFIG[gpu-process] = "-DENABLE_GPU_PROCESS=ON,-DENABLE_GPU_PROCESS=OFF,"
PACKAGECONFIG[hyphen] = "-DUSE_LIBHYPHEN=ON,-DUSE_LIBHYPHEN=OFF,hyphen"
PACKAGECONFIG[introspection] = "-DENABLE_INTROSPECTION=ON,-DENABLE_INTROSPECTION=OFF, gobject-introspection-native"
PACKAGECONFIG[jit] = "-DENABLE_JIT=ON -DENABLE_C_LOOP=OFF,-DENABLE_JIT=OFF -DENABLE_C_LOOP=ON,"
PACKAGECONFIG[jpegxl] = "-DUSE_JPEGXL=ON,-DUSE_JPEGXL=OFF,libjxl"
PACKAGECONFIG[journald] = "-DENABLE_JOURNALD_LOG=ON,-DENABLE_JOURNALD_LOG=OFF,"
PACKAGECONFIG[lcms] = "-DUSE_LCMS=ON,-DUSE_LCMS=OFF,"
PACKAGECONFIG[spellcheck] = "-DENABLE_SPELLCHECK=ON,-DENABLE_SPELLCHECK=OFF,enchant"
PACKAGECONFIG[wpe-legacy-api] = "-DENABLE_WPE_LEGACY_API=ON,-DENABLE_WPE_LEGACY_API=OFF,libwpe virtual/wpebackend,"
PACKAGECONFIG[libbacktrace] = "-DUSE_LIBBACKTRACE=ON,-DUSE_LIBBACKTRACE=OFF,libbacktrace"
PACKAGECONFIG[minibrowser] = "-DENABLE_MINIBROWSER=ON,-DENABLE_MINIBROWSER=OFF,wayland-native wayland-protocols wpebackend-fdo"
PACKAGECONFIG[mediasource] = "-DENABLE_MEDIA_SOURCE=ON,-DENABLE_MEDIA_SOURCE=OFF,gstreamer1.0 gstreamer1.0-plugins-good"
PACKAGECONFIG[mediastream] = "-DENABLE_MEDIA_STREAM=ON,-DENABLE_MEDIA_STREAM=OFF,gstreamer1.0 gstreamer1.0-plugins-bad"
PACKAGECONFIG[pdfjs] = "-DENABLE_PDFJS=ON,-DENABLE_PDFJS=OFF,"
PACKAGECONFIG[speech-synthesis] = "-DENABLE_SPEECH_SYNTHESIS=ON,-DENABLE_SPEECH_SYNTHESIS=OFF,flite"
PACKAGECONFIG[sysprof] = "-DUSE_SYSPROF_CAPTURE=ON, -DUSE_SYSPROF_CAPTURE=OFF,"
PACKAGECONFIG[system-sysprof] = "-DUSE_SYSTEM_SYSPROF_CAPTURE=ON, -DUSE_SYSTEM_SYSPROF_CAPTURE=OFF, sysprof"
PACKAGECONFIG[video] = "-DENABLE_VIDEO=ON,-DENABLE_VIDEO=OFF,gstreamer1.0 gstreamer1.0-plugins-base"
PACKAGECONFIG[webaudio] = "-DENABLE_WEB_AUDIO=ON,-DENABLE_WEB_AUDIO=OFF,gstreamer1.0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good"
PACKAGECONFIG[woff2] = "-DUSE_WOFF2=ON,-DUSE_WOFF2=OFF,woff2"
PACKAGECONFIG[remote-inspector] = "-DENABLE_REMOTE_INSPECTOR=ON,-DENABLE_REMOTE_INSPECTOR=OFF,"
PACKAGECONFIG[webrtc] = "-DENABLE_WEB_RTC=ON,-DENABLE_WEB_RTC=OFF,libvpx libevent libopus openh264"
PACKAGECONFIG[qtwpe] = "-DENABLE_WPE_QT_API=ON ${CMAKE_QT_OECONF},-DENABLE_WPE_QT_API=OFF,qtbase-native qtbase qtdeclarative libepoxy wpebackend-fdo ${QT_BUILD_DEPS}"
PACKAGECONFIG[unified-builds] = "-DENABLE_UNIFIED_BUILDS=ON,-DENABLE_UNIFIED_BUILDS=OFF,"
PACKAGECONFIG[thunder] = "-DENABLE_THUNDER=ON,-DENABLE_THUNDER=OFF,virtual/open-cdm"
PACKAGECONFIG[webxr] = "-DENABLE_WEBXR=ON,-DENABLE_WEBXR=OFF,openxr"
# Build option for WPE API 1.1
PACKAGECONFIG[wpe-1-1-api] = "-DENABLE_WPE_1_1_API:BOOL=ON,-DENABLE_WPE_1_1_API:BOOL=OFF,"
# Build option for WPE platform API
PACKAGECONFIG[wpe-platform] = "-DENABLE_WPE_PLATFORM=ON,-DENABLE_WPE_PLATFORM=OFF,libinput libxkbcommon wayland-native"
EXTRA_OECMAKE = " -DPORT=WPE -G Ninja"
# TODO: documentation and introspection are disabled by default because the are
# causing cross-compiling build errors
# PACKAGECONFIG:append = " ${@bb.utils.contains('DISTRO_FEATURES', 'api-documentation', 'documentation', '' ,d)} introspection"
# If SSE code compiles, assume it runs successfully (it can't actually run
# because of cross compiling)
EXTRA_OECMAKE:append:x86 = " -DHAVE_SSE2_EXTENSIONS_EXITCODE=0"
# Javascript JIT is not supported on ppc/arm/RISCV32/mips64
PACKAGECONFIG:remove:powerpc = "jit"
PACKAGECONFIG:remove:powerpc64 = "jit"
PACKAGECONFIG:remove:powerpc64le = "jit"
PACKAGECONFIG:remove:armv4 = "jit"
PACKAGECONFIG:remove:armv5 = "jit"
PACKAGECONFIG:remove:armv6 = "jit"
PACKAGECONFIG:remove:armv7a = "jit"
PACKAGECONFIG:remove:armv7ve = "jit"
PACKAGECONFIG:remove:riscv32 = "jit"
PACKAGECONFIG:remove:riscv64 = "jit"
PACKAGECONFIG:remove:mipsarchn64 = "jit"
PACKAGECONFIG:remove:mipsarchn32 = "jit"
PACKAGECONFIG:remove:loongarch64 = "jit"
# Javascript JIT is not supported on x86
PACKAGECONFIG:remove:x86 = "jit"
LDFLAGS:append:riscv64 = " -pthread"
FULL_OPTIMIZATION:remove = "-g"
LEAD_SONAME = "libWPEWebKit.so"
PACKAGES =+ "${PN}-web-inspector-plugin ${PN}-qtwpe-qml-plugin"
FILES:${PN} += "${libdir}/wpe-webkit*/injected-bundle/libWPEInjectedBundle.so"
FILES:${PN}-web-inspector-plugin += "${datadir}/wpe-webkit-*/inspector.gresource"
# nooelint: oelint.vars.insaneskip - ignored for convenience. We need to recheck if problem persist
INSANE_SKIP:${PN}-web-inspector-plugin = "dev-so"
# nooelint: oelint.vars.insaneskip - ignored for convenience. We need to recheck if problem persist
INSANE_SKIP:${PN}-qtwpe-qml-plugin = "dev-so"
# JSC JIT on ARMv7 is better supported with Thumb2 instruction set.
ARM_INSTRUCTION_SET:armv7a = "thumb"
ARM_INSTRUCTION_SET:armv7r = "thumb"
ARM_INSTRUCTION_SET:armv7m = "thumb"
ARM_INSTRUCTION_SET:armv7ve = "thumb"
# Extra runtime depends
# nooelint: oelint.vars.dependsordered - ignored for convenience
RDEPENDS:${PN} += "\
${@bb.utils.contains('PACKAGECONFIG', 'remote-inspector', '${PN}-web-inspector-plugin', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'gst_gl', 'gstreamer1.0-plugins-base-opengl', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'mediasource', 'gstreamer1.0-plugins-good-isomp4', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'webaudio', 'gstreamer1.0-plugins-good-wavparse', '', d)} \
${@bb.utils.contains('PACKAGECONFIG', 'video', 'gstreamer1.0-plugins-base-app \
gstreamer1.0-plugins-base-audioconvert \
gstreamer1.0-plugins-base-audioresample \
gstreamer1.0-plugins-base-gio \
gstreamer1.0-plugins-base-playback \
gstreamer1.0-plugins-base-typefindfunctions \
gstreamer1.0-plugins-base-videoconvertscale \
gstreamer1.0-plugins-base-volume \
gstreamer1.0-plugins-good-audiofx \
gstreamer1.0-plugins-good-audioparsers \
gstreamer1.0-plugins-good-autodetect \
gstreamer1.0-plugins-good-avi \
gstreamer1.0-plugins-good-deinterlace \
gstreamer1.0-plugins-good-interleave \
', '', d)} \
libgles2 \
"
RDEPENDS:${PN}-web-inspector-plugin += "\
shared-mime-info \
"
# Extra runtime recommends
RRECOMMENDS:${PN} += "\
ca-certificates \
ttf-dejavu-sans \
ttf-dejavu-sans-mono \
ttf-dejavu-serif \
${PN}-qtwpe-qml-plugin \
${@bb.utils.contains('PACKAGECONFIG', 'video', 'gstreamer1.0-plugins-base-meta gstreamer1.0-plugins-good-meta gstreamer1.0-plugins-bad-meta', '', d)} \
"
DEFAULT_PREFERENCE = "-1"
FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"
# https://commits.webkit.org/319279@main
PR = "r319279"
SRCREV = "93472ec12ee947b30c7ec3c176ca0e2fb6ba6ace"
SRC_URI = "git://github.com/WebKit/WebKit.git;protocol=https;branch=main \
file://0001-libpas-Only-include-stdatomic.h-when-compiling-with.patch \
file://0002-WebDriver-Guard-LOG_CHANNEL-check-with-LOG_DISABLED.patch \
"
S = "${WORKDIR}/git"
This file is self-contained on purpose; the idea is not to rely on any includes so that unpredictable behavior doesn’t happen in the future.
While the above file contains a lot of interesting details, the most interesting practical part is the last few lines. The SRCREV is set to point to the latest commit from the WebKit main branch (at the time
of writing). Along with that comes SRC_URI that specifies two patches required to make WPE WebKit compile.
The problem with the patches is that advancing SRCREV will likely make them unusable due to merge conflicts. Therefore, when advancing the revision, it’s recommended to remove patches from SRC_URI and face the compilation problems from scratch as
it’s very likely there will be new compilation problems anyway. Fortunately, nowadays LLMs can be used to fix any compilation problems by preparing custom patches just like the below ones (created by LLM as well). For example, most of the modern
models should manage to prepare proper patches just by pointing them to the above recipe and the temp directory with the latest logs from build commands.
Assuming one uses the wpewebkit_latest.bb above, the first patch needs to be created in: meta-webkit/recipes-browser/wpewebkit/wpewebkit/0001-libpas-Only-include-stdatomic.h-when-compiling-with.patch with the following content:
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Pawel Lampe <plampe@igalia.com>
Date: Mon, 17 Aug 2026 00:00:00 +0000
Subject: [PATCH] [libpas] Only include stdatomic.h when compiling with Clang
Some libpas .c files (e.g. jit_heap.c) are compiled as C++ via
set_source_files_properties(... PROPERTIES LANGUAGE CXX) in
Source/bmalloc/CMakeLists.txt, for TZone heap support. pas_utils.h
unconditionally does `#include <stdatomic.h>`, but GCC's own
<stdatomic.h> relies on the C11-only `_Atomic` keyword and has no
support for being included from C++ translation units (this was only
addressed in much newer GCC releases). Compiling any of the
CXX-tagged libpas .c files with GCC 13 therefore fails with:
error: '_Atomic' does not name a type
The header is only actually needed here for the Clang-specific
__c11_atomic_* intrinsics guarded by `#elif PAS_COMPILER(CLANG)`
further down in this file; the non-Clang (GCC) path uses the
__atomic_* builtins instead and does not need any of the types or
macros from <stdatomic.h>. Guard the include accordingly so GCC
builds (both plain C and the CXX-tagged libpas sources) are
unaffected by GCC's non-C++-aware <stdatomic.h>.
Upstream-Status: Pending
Signed-off-by: Pawel Lampe <plampe@igalia.com>
---
Source/bmalloc/libpas/src/libpas/pas_utils.h | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/Source/bmalloc/libpas/src/libpas/pas_utils.h b/Source/bmalloc/libpas/src/libpas/pas_utils.h
index 962634080930..9b98fda54f43 100644
--- a/Source/bmalloc/libpas/src/libpas/pas_utils.h
+++ b/Source/bmalloc/libpas/src/libpas/pas_utils.h
@@ -42,7 +42,15 @@
#endif
#include <limits.h>
+#if PAS_COMPILER(CLANG)
+/* GCC's <stdatomic.h> relies on the C-only _Atomic keyword and is not
+ * usable when this header is included from a translation unit compiled
+ * as C++ (some libpas .c files are compiled as C++, see bmalloc's
+ * CMakeLists.txt). It is only needed here for the Clang-specific
+ * __c11_atomic_* intrinsics below; the GCC path uses __atomic_* builtins
+ * instead. */
#include <stdatomic.h>
+#endif
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
--
2.43.0
The second patch should be: meta-webkit/recipes-browser/wpewebkit/wpewebkit/0002-WebDriver-Guard-LOG_CHANNEL-check-with-LOG_DISABLED.patch with:
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Pawel Lampe <plampe@igalia.com>
Date: Mon, 17 Aug 2026 00:00:00 +0000
Subject: [PATCH] [WebDriver] Guard LOG_CHANNEL check with
LOG_DISABLED/RELEASE_LOG_DISABLED
WebDriverService::handleRequest() unconditionally checks
LOG_CHANNEL(WebDriverClassic).state to decide whether it is worth
building the request/response log strings. However,
Source/WebDriver/Logging.h only declares the WebDriverClassic (and
other WebDriver) log channels inside:
#if !LOG_DISABLED || !RELEASE_LOG_DISABLED
On a release build (NDEBUG, so LOG_DISABLED is true) without journald
support and without OS_LOG/Android (so RELEASE_LOG_DISABLED is also
true) - the common configuration for an embedded Linux build without
the "journald" PACKAGECONFIG - that guard is false, so the channel is
never declared, and this direct, unguarded use of LOG_CHANNEL() fails
to compile:
error: 'LOG_CHANNEL_PREFIXWebDriverClassic' was not declared in this scope
RELEASE_LOG_INFO() itself already collapses to a no-op in that
configuration (see wtf/Assertions.h), so guard this manual
LOG_CHANNEL() state check with the same condition used to declare the
channel in Logging.h.
Upstream-Status: Pending
Signed-off-by: Pawel Lampe <plampe@igalia.com>
---
Source/WebDriver/WebDriverService.cpp | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Source/WebDriver/WebDriverService.cpp b/Source/WebDriver/WebDriverService.cpp
index 07ae385f33f4..2612d239a611 100644
--- a/Source/WebDriver/WebDriverService.cpp
+++ b/Source/WebDriver/WebDriverService.cpp
@@ -381,6 +381,7 @@ bool WebDriverService::findCommand(HTTPMethod method, const String& path, Comman
void WebDriverService::handleRequest(HTTPRequestHandler::Request&& request, Function<void (HTTPRequestHandler::Response&&)>&& replyHandler)
{
Function<void (HTTPRequestHandler::Response&&)> actualReplyHandler = WTF::move(replyHandler);
+#if !LOG_DISABLED || !RELEASE_LOG_DISABLED
if (LOG_CHANNEL(WebDriverClassic).state != WTFLogChannelState::Off) {
RELEASE_LOG_INFO(WebDriverClassic, "HTTP request %s %s (body=%zu bytes)", request.method.utf8().data(), request.path.utf8().data(), request.dataLength);
actualReplyHandler = [startTime = MonotonicTime::now(), replyHandler = WTF::move(actualReplyHandler)](HTTPRequestHandler::Response&& response) mutable {
@@ -388,6 +389,7 @@ void WebDriverService::handleRequest(HTTPRequestHandler::Request&& request, Func
replyHandler(WTF::move(response));
};
}
+#endif
auto method = toCommandHTTPMethod(request.method);
if (!method) {
--
2.43.0
Once the patches are added, wpewebkit latest should build correctly. However, to make use of it one needs a browser.
To demonstrate how easy the browser for WPE WebKit with Platform API can be, a very minimalistic one will be prepared below.
The first step is to create a directory:
mkdir meta-webkit/recipes-browser/wpe-browser/
Then a file: meta-webkit/recipes-browser/wpe-browser/main.cpp that implements the whole browser:
#include <wpe/webkit.h>
int main(int argc, const char *argv[]) {
g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, false);
g_autoptr(WebKitWebView) view = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW,
nullptr));
webkit_web_view_load_uri(view,
(argc > 1) ? argv[1] : "https://wpewebkit.org");
g_main_loop_run(loop);
return EXIT_SUCCESS;
}
Then a file: meta-webkit/recipes-browser/wpe-browser/CMakeLists.txt that describes how to build it:
cmake_minimum_required(VERSION 3.16)
project(wpe-browser CXX)
set(CMAKE_CXX_STANDARD 17)
include(GNUInstallDirs)
find_package(PkgConfig REQUIRED)
# The Wayland WPE Platform already depends on wpe-platform-2.0
pkg_check_modules(WebKitDeps REQUIRED
IMPORTED_TARGET
wpe-webkit-2.0
wpe-platform-wayland-2.0
)
add_executable(wpe-browser main.cpp)
target_link_libraries(wpe-browser
PRIVATE
PkgConfig::WebKitDeps
)
install(TARGETS wpe-browser RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
and finally a recipe file: meta-webkit/recipes-browser/wpe-browser/wpe-browser_1.0.bb that allows bitbake to build the browser and install it in the image:
SUMMARY = "Minimal WPE WebKit browser launcher"
DESCRIPTION = "A minimal launcher built on the WPE Platform API, displaying \
a URL given as its only argument (defaults to https://wpewebkit.org). \
Based on https://simonpena.com/blog/2026/03/20/getting-started-with-wpe-webkit/"
HOMEPAGE = "https://simonpena.com/blog/2026/03/20/getting-started-with-wpe-webkit/"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
FILESEXTRAPATHS:prepend := "${THISDIR}:"
SRC_URI = "file://main.cpp \
file://CMakeLists.txt \
"
S = "${WORKDIR}"
DEPENDS = "wpewebkit"
RDEPENDS:${PN} += "wpewebkit"
inherit cmake pkgconfig features_check
REQUIRED_DISTRO_FEATURES = "opengl wayland"
After all those steps, everything is ready to perform a build. This time the commands are executed in the SDK:
source poky/oe-init-build-env build
bitbake core-image-weston
cd tmp/deploy/images/raspberrypi3-64/
# flash to SD card based on preference
Once the image is built and flashed to SD card, and the SD card has been used to boot the Raspberry Pi, one can SSH into it, basically by:
ssh root@<IP>
Then, the browser should work out of the box. The first command to try is the one that doesn’t need the network and therefore is the simplest:
XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 wpe-browser 'webkit://gpu/stdout'
If the network is available, it’s worth starting simple and loading some HTTP page:
XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 wpe-browser 'http://info.cern.ch'
If that works as well, one can try the HTTPS one:
XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 wpe-browser 'https://igalia.com'
If there’s an issue with certificates, it’s likely due to broken date/time, so the correct one needs to be set using a command like:
date -s '2026-09-02 22:34:56'
Since the Platform API is the future of WPE, it’s worth using it already. As the above sections demonstrate, it hasn’t ever been easier to create a WPE-powered browser for embedded hardware. Moreover, nowadays as LLMs are at play,
using latest sources from main branch and quickly patching on demand is a possibility worth utilizing. With that, a broad sea of experimenting possibilities becomes wide open. However, it must not be forgotten that while main branch
is great for testing new web platform features implemented in WebKit, it’s not necessarily ideal for testing performance. In such case, it’s better to rely on releases and proper browser engine fine tuning for particular hardware one
plays with.
Update on what happened in WebKit in the week from August 31 to September 7.
This was another week focused on ironing out graphics issues in preparation for the upcoming 2.54.x release series. Did we say release? Here we have another set of packaged release candidates as well!
The “paint flushing” feature of the Web Inspector that shows the areas of the
layers that were painted is now
available. On the other hand, setting
WEBKIT_SHOW_DAMAGE=1 in the environment will show the areas of the window
that were rendered. For example, if the page is scrolled a little, the whole
window is rendered, but no layer has to be repainted.
Fixed visible pixel snapping in the GTK and WPE ports' Skia compositor by choosing linear instead of nearest-neighbor sampling for backing store tiles whenever a layer's transform no longer maps tile pixels onto screen pixels 1:1, matching what the Texture Mapper backend already got from its always-linear OpenGL code path.
Fixed a clipping bug in the
Skia-based compositor that caused layers with overflow: hidden set to
not clip blur and box-shadow filter outsets.
Fixed a repaint bug where content
changing behind an element with a software-rendered filter such as blur()
could leave stale pixels on screen, as with the glow effect that YouTube draws
around videos in dark/ambient mode. The fix restores proper tracking of which
layers act as the repaint container for a pixel-moving filter, so the repaint
area is expanded correctly again on WPE, GTK and macOS.
Release candidates WebKitGTK 2.53.92 and WPE WebKit 2.53.92 have been published, and they include polishing and a number of fixes that ensure that there will not be noticeable regressions brought in by the new Skia-based compositor and the damage tracking support—two of the main features of the upcoming stable release series. As the stable release dates approaches, we encourage people who try these preview versions to report issues in Bugzilla.
That’s all for this week!
It’s that exciting time of year again when we start considering Interop proposals for next year! We’re thrilled to invite you to help shape the future of web interoperability.
Interop 2027 is just around the corner, and we want your ideas to make the web platform even more awesome. Whether you’re a seasoned pro or a newcomer with fresh perspectives, we want to hear from you.
Don’t miss this opportunity to influence the direction of web standards and browser implementations.
There are two essential criteria that all successful Focus Area proposals will have — testability and web standards. If an idea for a needed web technology doesn’t already have a mature web standard (from the W3C, TC39, etc), then it’s too early for it to be a Focus Area for Interop. Those standards are what make it possible to create automated Web Platform Tests, and the test results are what make the Interop Dashboard.
If you have an idea for cool new web technology that should exist, but doesn’t, then look into proposing it (or helping it along) in the appropriate web standards venue. For example, if you want a new CSS feature to exist, check out filing or commenting on an issue at the CSS Working Group. That is where new CSS gets invented, and the details of exactly how every piece should work get determined and written down.
If you want to propose technology that has insufficient tests or testing infrastructure — consider proposing it as an Interop Investigation. Interop Focus Areas are for web technology that can be tested and scored, while Interop Investigations are assignments the group behind the Interop Project gives themselves to encourage improvements for the future.
The best submissions are typically:
font-size-adjust, not “typography”.Read the Interop Proposers Guide to learn more.
Take the time needed to fully make your case. Why is the focus area you are proposing important? If there are hundreds of things that could be considered, why does this one stand out? How does the current state of affairs inhibit web developers? How are users affected? Is this technology something a lot of sites use? How critical is fixing this sooner rather than later? And what evidence can you provide that a lot of people share your belief that this is important?
Don’t assume everyone reading the proposals has full knowledge of last year’s proposals, or has had the time to follow all the details of how developers are using (or not using) every web technology out there. Make the case in your proposal. Convince the Interop team!
Submit your proposal by filing an issue on Github. Feel free to collaborate with other people. If someone else has already proposed your idea, use the comments in GitHub issues to help further make the case. It’s far better to support an existing issue than have two issues for the same or very similar ideas. You can read proposals submitted in previous years to better understand the process.
The Interop Project is all about collaboration. By participating, you’re joining a global community of web developers, browser makers, and standards bodies working together to make the web more consistent and reliable across platforms. Share your insights as we make the web even more amazing together. We can’t wait to see your proposals.
WebKit for Safari 27 adds full spec compliance for top-level await. Before now, some of you may have run into unexpected “accessed before initialization” errors. We fixed it at the root by rewriting Safari’s module loader from the ground up so you can now confidently use await at the top level of your JavaScript modules. If top-level await wasn’t part of your toolkit before, this is a great time to give it another look.
Top-level await lets you use await at the top level of a module, enabling the same convenient use of Promises that async functions provide. Async functions provide support for await expressions to simplify complex Promise chains into linear sequences of code, and top-level await allows module authors to benefit from the same ease of use. Effectively, whenever an await expression is encountered, execution is paused until the awaited value is available, and in the meantime, control is returned to the caller of the async function. For ES modules, the analogy differs: when a module encounters a top-level await, any module that imports it is also suspended until the await resolves. However, sibling modules in the dependency graph that don’t depend on the awaiting module can still execute concurrently.
You can try all of this today. Download Safari Technology Preview 251 or grab Safari 27 beta, and try integrating top-level await into your web apps. It just works! The improvements go beyond just top-level await: with the module loader rebuilt on the right foundation, ES modules as a whole are now something you can build on in Safari without a second thought. And once Safari 27 ships, you’ll be able to lean on top-level await and ES modules across your projects, in production, for everyone.
Top-level await is a complex feature implemented inside the module loader machinery. The ECMAScript specification’s section of modules leaves some parts up to the host—this includes the mechanism of fetching modules, which in practice will be done over the network by web browsers or from the local filesystem by JavaScript runtimes like Node.js and Bun. The WebKit team wrote Safari’s module loader long ago, back during the days of the WHATWG Loader proposal (last updated January 2016), and we relied on the proposal’s specification of the host-defined functionality. This worked during the early history of ES modules, when module execution was purely synchronous and top-level await didn’t exist. However, ECMAScript 2022 was later released, introducing top-level await as a feature. By this point, the WHATWG Loader proposal had effectively faded into obscurity after being superseded by the ECMAScript standard’s module section, but Safari’s module loader was still based on it. Top-level await was implemented in terms of a proposal that was abandoned before any support for async/await existed in ECMAScript, instead of in accordance with the ECMAScript standard’s algorithms for asynchronous module execution. This mismatch caused subtle bugs that we couldn’t fully resolve for years, despite multiple attempts. Instead of continuing to patch a foundation that couldn’t support the feature, we decided to rebuild it correctly.
A major cause of Safari’s top-level await bugs was in how the module loader chose the order in which to load and evaluate modules. This code example demonstrates the issue:
// main.js
async function load(index) {
try {
print("Importing", index);
const module = await import("./test-module.js");
print("Imported", index);
try {
print(`Keys for ${index}:`, Object.keys(module));
} catch (e) {
print("Accessing", index, "failed:", e.message);
}
} catch (e) {
print("Importing", index, "failed:", e.message);
}
}
try {
const imports = Array.from({ length: 3 }, (_, i) => {
return load(i + 1);
});
await Promise.all(imports);
} catch (e) {
print("Test failed:", e);
}
// test-module.js
await new Promise(resolve => setTimeout(resolve, 10));
export function someFunction() {
return "Hello!";
}
export const someArray = [];
Its purpose is to dynamically load the same module three times in a row, printing the names of the module’s exports each time. It’s supposed to be ordered 1, 2, 3, but if the code is run with the old module loader, something unexpected occurs:
Importing 1
Importing 2
Importing 3
Imported 2
Accessing 2 failed: Cannot access 'someArray' before initialization.
Imported 3
Accessing 3 failed: Cannot access 'someArray' before initialization.
Imported 1
Keys for 1: someArray,someFunction
There are two things going wrong here. First, the order in which the imports complete is wrong. Instead of the expected 1, 2, 3, the order is 2, 3, 1. Second, there are strange errors about accessing a value before it’s initialized. Both problems are caused by the same bug.
When the module loader starts loading the module with top-level await the first time, it begins to execute it, and then pauses execution when it reaches the await. It then returns control to the main module, which begins the second import. The promise for the second import shouldn’t resolve until after the first import is done evaluating, but due to a bug in the old module loader, it resolves immediately. This leads to the second import finishing first, and because the evaluation of the imported module hasn’t completed yet, the code to print the module’s keys accesses uninitialized exports, causing an exception. The same happens with the third import. After those two fail, the first import finishes. This time, the evaluation has completed, so it’s able to successfully print the keys of the module’s exports.
With the new module loader, the output is what you’d expect:
Importing 1
Importing 2
Importing 3
Imported 1
Keys for 1: someArray,someFunction
Imported 2
Keys for 2: someArray,someFunction
Imported 3
Keys for 3: someArray,someFunction
The old module loader was written in JavaScript as a self-hosted builtin. This had some advantages: unlike native code, JavaScript builtins can be inlined into the user code that invokes them, and it’s possible to avoid the performance penalty paid when crossing the boundary between JavaScript and C++. In addition, creation of objects is faster from JavaScript, as it’s sometimes possible to eliminate the heap allocation.
However, we later found multiple drawbacks with self-hosted JavaScript: it’s slower to start up because it has to be compiled at run time, unlike native code, which is fully compiled well in advance. In addition, builtins have inherently wide usage characteristics, which makes it harder for JavaScriptCore’s optimizing JIT compilers to exploit patterns in usage, and the module loader code isn’t a hot path, which further reduces the benefit of compiling at runtime. As a result, overall performance is less stable and predictable than it is for C++. When we rewrote the module loader, we chose to drop the self-hosted builtin approach in favor of fully native code.
In January 2026, we began rewriting the module loader. Because the consensus at that point had been that self-hosted JavaScript was not the ideal approach for the module loader, we started the process by deleting the entire JavaScript file that contained the old module loader.
After that, we began implementing the module loader operations one by one, translating the pseudocode defined in the ECMAScript specification into C++. As a guide through ordering which functions to implement first, given that the module loading machinery is a complex state machine, we read through the specification and took note of how the functions called each other and assembled a flow graph. This gave a starting point.

Leaf functions like ExecuteModule and ModuleRequestsEqual were ideal to implement early because they didn’t depend on any other functions. After a few weeks, the new module loader was able to handle the most common cases and a draft pull request was put up.
Because the point of the rewrite was to improve the module loader’s reliability, thorough testing was essential. Engineers at Bun, whose runtime is built on JavaScriptCore and inherited the old module loader’s problems, graciously provided test cases they’d collected that demonstrated incorrect behavior. It was simple to adapt these to run with JavaScriptCore’s command line shell (jsc) and integrate them as tests.
To stress test the module loader’s performance and functionality, we wrote a fuzzer that produced complex graphs of modules (some with top-level await, others without) and import statements. The goal was to ensure that the observable effects of the module loading process were correct. We did this by generating large graphs and comparing the output of JavaScriptCore with the new module loader to the output of other JavaScript engines. If the text output matched byte-for-byte with other engines’ outputs, we could be sure that the new module loader was handling the test case correctly. And indeed we found in every tested example that the new module loader correctly handled the fuzzer output. Once all the module-related tests from test262 started passing and we fixed many previously failing module tests from WPT as well with no regressions, the new module loader was ready for merging. By this point, we had for a few weeks been daily driving a build of Safari with the new module loader integrated with no issues.
When we release Safari 27, you can start shipping web apps that take advantage of top-level await’s benefits. Until then, download the beta and try integrating top-level await into your projects. If you run into any issues, we’d love to hear your feedback. As always, bug reports can be submitted at bugs.webkit.org.
Update on what happened in WebKit in the week from August 24 to August 31.
After such a packed edition last week, we can relax with an even more packed installment this week! The team has been moving full steam ahead with the Layer-Based SVG Engine, and graphics improvements in general, but we also had a handful of other updates, such as SDK updates, a new WebsitePolicies API, and more.
Add new ChildChange types for moveBefore(). This addresses issues with the in-progress moveBefore() implementation where scripts would sometimes execute erroneously.
Added alpha channel support to color input choosers.
Fixed Service Worker static routes on non-Apple ports.
Added a WebsitePolicies:upgrade-to-https-policy property. When enabled this policy will automatically try using HTTPS even for HTTP websites (excluding localhost or IPs). The policy can be configured either to allow automatically falling back to HTTP if that fails, or to consider all HTTPS failures fatal for the best security.
Fixed SVG text being rasterized for the wrong resolution in zoomed standalone SVG documents in the Layer-Based SVG Engine (LBSE). vector-effect: non-scaling-stroke on <text> no longer comes out too thick, and the resulting metrics match the legacy SVG engine.
Fixed viewport clipping in the Layer-Based SVG Engine (LBSE), where a nested <svg> or a <marker> applied its viewport clip even when its content already fitted inside, and since that clip is not pixel-snapped its edge could fall between two device pixels and cut into whatever was drawn right at the viewport border. Painting now skips a clip that removes nothing, which eliminates a class of subtle pixel differences against the legacy SVG engine.
Fixed opacity animations not damaging descendant layers in the GTK and WPE ports, where a descendant that paints outside its parent's bounds kept its stale pixels on screen. The mask-specific damage handling was generalized into a single group-property path shared by opacity, filters, mask blend modes and replicas, which now damages the layer plus the overlap region of its whole subtree.
Added a cycle-analysis subcommand to webkit-sysprof, which draws every frame cycle of a capture as a bar of cells colored by the mark covering that moment on the main thread, showing whether a slow frame stalled on layout, a long timer or rasterization instead of only reporting that it was slow.
Removed redundant text updates for the text children of elements using display: contents, which previously got one on every style resolution regardless of whether their style actually changed. This removes dozens of useless updates per style recalculation in Web Component applications, where every <slot> uses display: contents.
Fixed tile image caching in the Skia compositor by regenerating the cached SkImage whenever a a tile's contents are updated and by adding a texture release callback that keeps the texture valid for as long as the image references it.
Switched video DMA-BUF buffers to Skia promise images in the Skia compositor, so the texture backing a video frame is only created at the point Skia actually draws it, which works for these buffers because the underlying DMABufBuffer can be kept alive until the promise image is released.
Fixed a hang in the Layer-Based SVG Engine (LBSE) when two SVG <pattern> elements reference each other through href, or one references itself: collecting the inherited pattern attributes now remembers which patterns it has already visited, the same cycle detection that gradients have always had. The walk also resolves every reference in the tree scope of the pattern it started from, so a pattern referenced from inside a shadow tree now inherits the right attributes.
Skipped anchor-positioning bookkeeping during style resolution when a document uses no anchor positioning at all, avoiding two hash lookups per styled element.
Cached the SVG viewport size used to resolve lengths in the Layer-Based SVG Engine (LBSE), instead of recomputing the nearest <svg> element's view box rectangle once per shape per frame. The viewport is invariant across a flush and identical for every shape under the same <svg>, so caching removes redundant work.
Removed the code path for GPU rendering without Deferred Display Lists (DDL) in the Skia compositor, so accelerated painting always records into a display list and no longer needs to create GL contexts on worker threads.
Fixed red and blue appearing swapped in non-accelerated video with the Skia compositor, by allocating the video frame's BitmapTexture with the BGRA layout flag and applying that flag when the buffer is turned into a Skia image.
Bumped the GTK and WPE developer SDK from v9 to v11, bringing GStreamer 1.28.5, sparkle-cdm 2026.2 and libsoup 3.7.2. Be sure to update your local wkdev-sdk container using wkdev-update to make sure your development environment matches what the CI is testing.
That’s all for this week!
Safari Technology Preview Release 251 is now available for download for macOS Golden Gate and macOS Tahoe. If you already have Safari Technology Preview installed, you can update it in System Settings under General → Software Update.
This release includes WebKit changes between: 317935@main…319386@main.
href being exposed as links. (318186@main) (181924326)@supports at-rule(...), which tests if a CSS at-rule is supported. (318482@main) (87884897)object-view-box property. (318981@main) (132624217)property-scoped and property-index-scoped caching keywords in random(). (318960@main) (176395332)random-item() function. (319180@main) (181816677)@container queries. (318233@main) (182249814)white-space-trim in the white-space shorthand. (318486@main) (182739052)data: URL filter references on HTML elements. (317991@main) (183059589)HighlightRegistry.highlightsFromPoint(). (318758@main) (184150480)random() in custom properties. (319039@main) (184715508)ident() function. (319049@main) (184732904)inherit() function. (319062@main) (184738624)corner-shape property. (319205@main) (184950973)max-width constrained box when the page is zoomed out, by scaling the minimum font size with the zoom factor. (318509@main) (66569768)column-gap overflowing a parent constrained by max-width. (318009@main) (137422489)@scope picking the wrong scoping roots when <scope-start> contains :has(:scope ...). (318711@main) (138495467)size property in @page when both a page size and orientation are given. (318083@main) (162168233)outline-offset not being snapped as a line width. (319236@main) (180054053)min-width and max-width. (318264@main) (180064739):focus-visible being shown after calling focus() inside an already focused ancestor. (318775@main) (181570771)min-height when it exceeds max-height. (318676@main) (182381903)text-underline-offset being mispositioned for highlighted text in vertical writing modes. (318107@main) (182889143)@container style queries evaluating root-relative lengths incorrectly. (318144@main) (183094796)::part() in @container queries to be the element itself. (318620@main) (183154183)::highlight() background fills in vertical writing modes. (318177@main) (183496114)wrap-reverse flex container coming from the wrong flex line. (318154@main) (183516222)baseline and last baseline alignment groups. (318165@main) (183536181)-webkit-border-image width flag being lost when a length edge precedes a non-length edge. (318214@main) (183584830)block-ellipsis of a clamped block being styled from the wrong block formatting context. (318437@main) (183606151)::highlight() pseudo-elements not inheriting from the parent element’s highlight. (318779@main) (183702060)clip on a child being ignored when determining whether an ancestor’s background is obscured. (318636@main) (183841775)var() not behaving as an arbitrary substitution function. (318912@main) (183913427)line-height to more closely match how it was specified. (318710@main) (184232888)font-* properties resolving font-relative units against their own font instead of the parent element’s font metrics. (319052@main) (184336424)rangeStart and rangeEnd animation attributes accepting length values that are not computationally independent when set from script. (318815@main) (184363719)-webkit-text-stroke not applying to highlighted text. (318976@main) (184375039)ViewTimelineOptions inset member accepting length values that are not computationally independent when set from script. (318883@main) (184504829)::highlight() not repainting after a CSS rule change. (319169@main) (184643272)::highlight() not repainting when a highlight’s range is changed with CSS.highlights.set(). (319154@main) (184672580)@function body not inheriting computed custom property values from the calling context. (319031@main) (184701762)@function being treated as a cycle. (319034@main) (184711017)random() caching key not including the custom property name. (319096@main) (184722866)auto <random-key> not being element-scoped, so every element shared the same item. (319153@main) (184845561)percentage value from -webkit-line-clamp. (318858@main) (159808285)margin-trim support from flex containers, following the CSSWG resolution limiting it to block and multi-column containers. (318654@main) (184151309)margin-trim support from grid containers, following the CSSWG resolution limiting it to block and multi-column containers. (318690@main) (184164505)ctx.font not re-resolving relative font sizes when set to the same string after the canvas element’s style changed. (318753@main) (184194378)font keywords larger and smaller scaling by 1.02 instead of approximately 1.2. (318798@main) (184312611)popover=hint. (318958@main) (184548373)display: none not opening when its <label> is clicked. (318723@main) (100476630)createContextualFragment() leaving nested <head> and <body> elements behind when stripping a stray <html> element. (317962@main) (183163523)<label> default action triggering when clicked interactive content removes itself. (318827@main) (184381262)<dialog> is closed. (319173@main) (184751412)Iterator.prototype.includes(). (318184@main) (173846280)monthCode fields in Intl and Temporal calendars. (318428@main) (182753821)traceStack option on WebAssembly.Exception and for Exception.stack. (318397@main) (183342491)import defer. (318481@main) (183892394)Iterator.prototype.join(). (318502@main) (183892495)Iterator.prototype.chunks() and Iterator.prototype.windows(). (318479@main) (183892687)Iterator.zip() and Iterator.zipKeyed(). (318477@main) (183892768)type: text. (318950@main) (184443879)^ inside a group that can match empty failing to anchor the pattern. (317981@main) (183300022).*. (317983@main) (183300106)JSON.stringify() regression in how toJSON is checked. (318072@main) (183400665)Iterator.zip to use null-prototype objects for its options object and synthetic underlying iterator. (318061@main) (183418070)Array.from() fast path ignoring a Symbol.iterator override on set.keys() and set.values(). (318374@main) (183801651)v mode class set operations in regular expressions. (318416@main) (183879621)Temporal.PlainDate construction with years outside the supported range. (318504@main) (183960408)TypeError regression when passing monthCode to Temporal calendar methods. (318566@main) (183980712)Temporal constructors read newTarget.prototype. (318635@main) (184072358)String.prototype.localeCompare() collator cache going stale after a language change. (318616@main) (184096794)Temporal skipping the epoch range check when resolving a time inside a daylight saving time gap. (318663@main) (184140083)Temporal time zone identifier parsing to match the specification’s parse records. (318741@main) (184169471)Array.prototype.sort() not being stable when called without a comparator. (318682@main) (184189734)Iterator.prototype.join() to match the latest specification. (318806@main) (184321382)Iterator.prototype.chunks() and Iterator.prototype.windows() to match the latest specification. (318802@main) (184321801)Intl.Locale collections not being sorted. (318808@main) (184322384)formatToParts() omitting the era separator that format() inserts. (318847@main) (184382546)import defer implementation to match the updated proposal. (318875@main) (184423667)\P{...} on the right side of a v mode && or -- operation being treated as a union. (318854@main) (184443379)Temporal.Duration rounding decisions being made on doubles instead of exact integers. (318901@main) (184472920)Temporal returning plausible values instead of throwing when an underlying calendar operation fails. (318953@main) (184567556)WebAssembly.Global value setter not treating a missing argument as undefined. (318967@main) (184608074)u and v modes. (319064@main) (184762230)HTMLMediaElement.removeTextTrack(). (318250@main) (183365541)generateKeyRequest() returning an incorrect destination URL. (317970@main) (183305929)requestFullscreen() and requestPictureInPicture() not consuming user activation. (318673@main) (183621577)MediaRecorder dropping pending video frames when recording is stopped. (318595@main) (183975207)start or end text alignment to be 50%. (318610@main) (184039946)MediaRecorder dropping a frame whenever the writer input reports that it is not ready. (318778@main) (184271782)to property on NavigationTransition. (319142@main) (184792753)sourceElement reporting the deepest clicked element instead of the element responsible for the navigation. (319005@main) (184581613)NavigationHistoryEntry returning null instead of the empty string for url, key, and id when the document is not fully active. (319007@main) (184590003)navigateEvent.scroll() not scrolling to the beginning of the document when the fragment does not exist. (319068@main) (184595100)navigation.activation.from not being null when navigating away from the initial about:blank. (319217@main) (184624752)navigateerror not firing for a navigation started from a navigateerror handler. (319222@main) (184633618)fetch(). (318236@main) (183400867)download attribute click in the same task. (319105@main) (165498647)XMLHttpRequest.getAllResponseHeaders() returning the previous response’s headers when the object is reused. (318217@main) (183561750)XMLHttpRequest.responseURL still returning the aborted request’s URL after calling abort(). (318241@main) (183587043)XMLHttpRequest.send() setting a Content-Type request header on GET and HEAD requests when passed a URLSearchParams. (318424@main) (183801778)imagesrcset not overriding href on <link rel="preload" as="image">. (318544@main) (183945535)<link rel=preload as> keywords not being matched ASCII case-insensitively. (318584@main) (184013239)connect-src Content Security Policy directive instead of object-src. (318797@main) (184258237)host-source of the form *:*/path not matching. (318612@main) (184902666)text-align: justify justifying the last line of a block. (318562@main) (183976364)unicode-bidi: plaintext and text-indent: each-line. (318886@main) (184505835)text-wrap: balance for the whole container. (318893@main) (184508888)line-clamp not counting the lines of a block level box on a line. (318921@main) (184545926)<fieldset> on a line taking its legend’s overhang twice. (319033@main) (184714637)data: URL filter references on SVG elements. (317538@main) (114823248)DOMMatrix2DInit dictionary to SVGMatrix.multiply(). (318109@main) (180896205)clip-path, marker, and paint servers. (318543@main) (183941298)auto sizing of <svg:image> to follow the CSS default sizing algorithm. (318548@main) (122586485)SVGSVGElement.currentScale being a no-op on an outermost <svg> that is not the document root. (318762@main) (180889449)SVGLength.value returning negative numbers for r, rx, ry, width, height, and textLength, which disagreed with getComputedStyle(). (317963@main) (181108927)clip-path basic shape being mispositioned on a <foreignObject> with a non-zero x or y. (318904@main) (182755161)feDropShadow ignoring an update to the Y component of stdDeviation when X changes. (318281@main) (183678196)<animateMotion> ignoring a numeric rotate value. (318565@main) (183832405)x and y on <foreignObject> having no effect. (318669@main) (184172530)clip-path basic shape being mispositioned on a rotated or scaled SVG element. (318924@main) (184494108)<animateMotion> ignoring a zero-length motion path. (318963@main) (184569273)SVGPathSegList collection and the SVGPathSeg family of interfaces from SVGPathElement, matching SVG 2. (318760@main) (115039851)element.scroll() and element.scrollTo() not scrolling inside a content-visibility: hidden subtree. (319219@main) (184935071)Sec-Fetch-Dest header on service worker script requests. (318237@main) (181909962)ReadableStream. (318043@main) (183317580)InvalidStateError message thrown by selection methods to explain what is invalid. (318520@main) (134645414)resize event when an iframe transitions from display: none to visible. (318713@main) (174638256)timeOrigin being skewed for Navigation Timing entries when the browsing context group changes. (318057@main) (179332119)getBoundingClientRect() returning a non-zero rect for SVG elements inside hidden containers. (318744@main) (183425103)FileSystemWritableFileStream. (318130@main) (183489019)FileReader.abort() being silently ignored on a reader reused after a completed read. (318288@main) (183695958)tabIndex when tabindex is changed to an invalid value. (318303@main) (183696552)SharedWorkerGlobalScope prototype not being an immutable prototype exotic object. (318684@main) (184190755)scrollend event not firing when wheel-scrolling a subframe. (318805@main) (184352772)exponentialRampToValueAtTime() caused by accumulating the ramp value. (317955@main) (124498013)decodeAudioData() failing with an EncodingError for valid M4A files whose ftyp brand is not mp4. (318005@main) (183148186)registerProcessor() error messages so they name the offending AudioParam instead of the processor. (317948@main) (183189781)OscillatorNode with a-rate automation starting out of phase by honoring sub-sample start times. (317953@main) (183194827)AudioBufferSourceNode output channel count to account for whether the node is actively processing. (317956@main) (183245229)AudioScheduledSourceNode not firing the ended event when it is not connected to the destination. (318515@main) (183986981)AudioParam input to AudioBufferSourceNode.detune diverging from the equivalent automation. (318516@main) (183987621)AudioContext.close() not rejecting when called on an already-closed context. (318522@main) (183989479)AudioWorkletGlobalScope.currentFrame reporting 0 for a worklet created while the context is suspended. (318601@main) (184081765)AudioWorkletProcessor being torn down too early after process() returns false. (318621@main) (184107806)AudioWorkletNode processorerror event missing filename, lineno, and colno. (318705@main) (184224032)MediaStreamAudioSourceNode capturing the wrong track and continuing to play after the track ends. (318706@main) (184224142)exponentialRampToValueAtTime() at a non-frame-aligned time rendering NaN. (318868@main) (184434164)ConvolverNode.buffer to null not releasing the impulse response. (318870@main) (184434803)externally_connectable.matches. (318361@main) (183642164)setAccessLevel() on the local and sync storage areas. (318980@main) (183680404)scripting.ExecutionWorld. (319009@main) (184284891)browser.alarms API. (318493@main) (183770530)console.record() options being treated as if they were present. (318555@main) (184029716)<canvas> elements not being distinguished from CSS canvas client nodes. (318794@main) (184346714)ImageDecoder API. (319179@main) (184857272)vec3 value. (318098@main) (183426431)executeBundles() is called with bundles containing indirect draws. (318538@main) (183458069)textureDimensions(t, level) clamping the mip level one past the last valid level. (318273@main) (183673818)GPUQueue.copyExternalImageToTexture() uploading indexed PNG images incorrectly. (318952@main) (184529653)min and max are given in a capture constraint range. (318046@main) (182968516)setSinkId() reporting success without switching audio output for a single-track MediaStream. (318149@main) (183147942)getUserMedia() interrupting the microphone system-wide when echoCancellation is disabled. (318164@main) (183517329)ImageCapture.takePhoto() failing permanently after a single transient capture error. (318260@main) (183640839)Update on what happened in WebKit in the week from August 17 to August 24.
Another periodical packed with updates on the graphics, multimedia, and tooling fronts. Which sure has to do with preparing for the upcoming 2.54.x release series, that now has release candidates published. Also, do not miss a new stable release being published with fixes for security issues, and make sure to update.
Extended the webkit-sysprof analyze
tool cover the remaining marks emitted along the rendering pipeline, from
RenderTreeBuild and CompositingUpdate through FinalizeRenderingUpdate,
RenderLayerTree and WaitForCompositionCompletion down to the individual
tile marks, pulling the tile count and the dirty region out of the mark
messages as statistics next to the durations. The statistics tables also gained
mean and median columns, so a captured trace now shows where the time in a
frame actually goes.
Fixed test
fast/canvas/canvas-composite-text-alpha.html, and improved the state of
several other tests which relied on setTimeout().
Implemented moving steps for
<option> elements.
Add support for asynchronous scrolling with touch events.
Touch events are now dispatched to the EventDispatcher thread in the Web
Process, enabling smooth scrolling even when the main thread is busy.
Additionally, this change fixed a bug where precise scrolling delta wheel
events, dispatched by touchpads, failed to trigger asynchronous scrolling.
Fixed several accessibility related tests and added support for missing keywords in the GLib-based ports (GTK & WPE):
GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.
Ensure that multimedia on pages restored from the back-forward cache is correctly processed without errors or reloading.
The robustness level is now queried from the CDM (if supported) when using Encrypted Media Extensions (EME).
Added video rendering support for Qualcomm hardware-accelerated decoders when the Skia compositor is in use, complementing the earlier TextureMapper-only support that left such videos blank with the new compositor.
Video frame processing now relies on the driver's implicit YUV to RGB conversion, steered by the colour space and sample range hints taken from the frame colorimetry, with a hint-free import as fallback.
Fixed how <feImage> paints its referenced
element in the Layer-Based SVG Engine
(LBSE), which still used a helper written for the legacy SVG engine, where SVG
content never had layers, so it used to paint renderers directly and skipped
any child that owns a RenderLayer under LBSE, silently dropping its
opacity, mask, filter or 3D transform. The referenced content is now painted
through the layer tree, the way <mask>,<clipPath>, <pattern> and
<marker> content already is.
Made SVG mask no longer force a
RenderLayer in the Layer-Based SVG
Engine (LBSE), mirroring the earlier change for
clip-path. A mask is now applied
during painting by SVGNonLayerClippingAndMaskingScope, which opens one
transparency layer capturing the renderer's foreground and composites the mask
over it afterwards, and which also absorbed the clips that cannot be expressed
as a path. A container with a mask keeps its layer, since the mask covers its
whole subtree, while a leaf stays layer-free. This further reduces the layer
overhead that has been holding LBSE back against the legacy SVG engine.
Skipped scroll coordination for composited layers that have no scrolling role, where the per-layer update previously walked every branch to detach roles the layer had never registered for, only to hand back the parent node ID unchanged. Cutting that work out shortens every compositing update, which matters for composition-heavy workloads.
Fixed rendering of an outermost <svg> with an empty
viewBox in the Layer-Based SVG
Engine (LBSE), which per the SVG
specification should
disable painting when the width or height is zero, matching what the legacy
engine already did.
Skipped scroll coordination work for layers that have no scrolling
role during compositing updates, where
updateScrollCoordinationForLayer previously walked every branch to detach
roles the layer had never registered for, only to hand back the unchanged
parent node ID.
WebKitGTK
2.52.6 and
WPE WebKit 2.52.6 have
been released, including a number of fixes for security issues covered in the
accompanying security advisory WSA-2026-0005
(GTK,
WPE). It is recommended
for everybody to update to these stable releases.
Stabilization for the upcoming 2.54.x release series for both the GTK and WPE ports is ongoing, with the first stable release, 2.54.0, expected around mid-September 2026. In the meantime release candidates WebKitGTK 2.53.91 and WPE WebKit 2.53.91 have been released.
Those interested in previewing the work done by the team in the last half year, including the new Skia-based compositor that is expected to eventually replace the aging TextureMapper, may want to give them a try and report any issues found in Bugzilla.
That’s all for this week!
Update on what happened in WebKit in the week from August 10 to August 17.
Following an extra packed periodical, this week we get back to a more regular pace with two nice bugfixes, and a new tool to analyze WebKit performance on Linux!
The webkit-sysprof toolkit landed in main thus introducing a set of tools for processing Sysprof .syscap capture files recorded from WebKit (GTK/WPE ports). It extracts marks (timeline events) and counters (time-series metrics) from a capture and lets one dump, summarize, analyze, or plot delta-time histograms for them.
Fixed filters specified on the outermost <svg> element in the Layer-Based SVG Engine (LBSE), where a filter: url(...) reference on an SVG root was silently dropped because the layer code skipped it, as the legacy engine used to apply it by itself. The filter region is now resolved against the SVG root's border box in its container's coordinate system, since the outermost <svg> is a replaced element in the CSS box tree, not part of the SVG user space its children live in.
Avoided serializing gradient and pattern transforms just to answer a presence check in the Layer-Based SVG Engine (LBSE). Asking hasAttribute() whether gradientTransform or patternTransform was specified forced the transform list to be serialized into the attribute map whenever the base value was changed through the SVG DOM, even though that string is never read back, so the check is now answered directly from the typed accessor.
That’s all for this week!
Safari Technology Preview Release 250 is now available for download for macOS Golden Gate and macOS Tahoe. If you already have Safari Technology Preview installed, you can update it in System Settings under General → Software Update.
This release includes WebKit changes between: 317507@main…317934@main.
<textarea> whose value ends in a line break. (317784@main) (182912872)spaces value of the ruby-overhang property, which limits annotation overhang to adjacent spaces and punctuation, with none now an alias for spaces. (317569@main) (181808420)content property on the ::marker pseudo-element. (317570@main) (182750380)text-decoration-inset property. (317571@main) (182750933)white-space-trim property. (317573@main) (182751076)wrap-inside property. (317574@main) (182751559)list-style-position: inside marker glyph being painted one device pixel away from the equivalent inline text. (317789@main) (182447949)text-decoration specified on ::selection never being painted. (317673@main) (182757292)::selection not being painted when the selected text had no decoration of its own. (317695@main) (182864748)max-content sized block collapsing to zero width when its child used an orthogonal writing mode. (317781@main) (183026896)text color value; use canvastext instead. (317687@main) (100364547)image/svg+xml data with the Clipboard API, sanitizing the markup on both read and write. (317528@main) (137553836)using and await using declarations, Symbol.dispose, DisposableStack, and AsyncDisposableStack. (317894@main) (103209363)Iterator.zip() and Iterator.zipKeyed() from the Joint Iteration proposal. (317886@main) (180967133)pause and ended event firing on a live HLS stream when playback reached the live edge. (317626@main) (100556746)REGION or STYLE block. (317811@main) (181977285)ReadableStream body with fetch(). (317724@main) (182510365)duplex option on Request, which is required when uploading a ReadableStream body. (317768@main) (182802330)scrollIntoView() on an inline element sharing a line with a much taller element scrolling to the top of the line box instead of to the element. (317933@main) (178491868)put() that fails a unique index constraint deleting the record it would have overwritten, leaving the object store with neither the old nor the new record. (317632@main) (181513905)get() being rejected in an iframe inside a focused window. (317656@main) (109191868)Cache-Control header already set by the page. (317814@main) (183072123)Update on what happened in WebKit in the week from July 28 to August 10.
Quite a packed pair of weeks this time! The range of updates is big, but some highlights are the handful of Layer-Based SVG Engine updates, performance improvements, and the new WPE APIs. Finally, the Web Engines Hackfest recordings are now published!
Fixed webkit_website_data_get_size() looking up sizes for localStorage, indexedDB, and the DOM Cache.
Enabled support for the File System and Storage APIs.
Added WEBKIT_WEBSITE_DATA_FILE_SYSTEM API to enable fetching/clearing File System data sites use.
GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.
Landed a follow-up fix for the h264 edit list support to prevent regressions on YouTube MSE Conformance Tests 2019 when using older versions of GStreamer.
Avoided a spurious seek to zero when seeking to the end of an audio/video when playback starts.
Recovered a MotionMark compositing regression in the Skia backend, where respecting damage information during compositing cost about 20% on the composition suite and more than 50% on two of its tests. Restricting a draw to the damaged area means splitting it into source-rect-to-destination-rect pieces or drawing it under a device-space clip, and neither is needed when the damage already covers the whole draw, which is the common case in those tests because a composited layer is much smaller than a damage grid cell. Those draws are now issued exactly as they would be with damage turned off, avoiding a clip path that flushed the image set batch and left it an order of magnitude smaller, and with the regressions gone, using damage information for compositing was enabled again along with unifying damaged regions that are sent to the system compositor.
Skipped a per-frame visual overflow recomputation in the container paint cull of the Layer-Based SVG Engine (LBSE). The cull used a cached overflow rect that was recomputed by unioning all descendant bounds on a miss, which happened every frame for containers with an animated transform. It now only runs when the rect is already cached.
Skipped the outline paint pass for SVG renderers without an outline in the Layer-Based SVG Engine (LBSE). Every shape used to be painted twice per frame, the second pass being a no-op in the common outline-free case, so guarding it behind hasOutline() removes a redundant traversal from every frame.
Fixed masked SVG content being cut off at the edges in the Layer-Based SVG Engine (LBSE), where the mask image was sized over the enclosing integer rect of the mask content bounds in device space while the transparency layer clip was computed differently, losing the outermost pixels. An SVG renderer that is a box (<text>, <foreignObject>) also used the CSS mask clip rect derived from the border box, which leaves out SVG content spilling outside it, and now uses the visual overflow rect instead
Fixed most of the remaining <mask> issues in the Layer-Based SVG Engine (LBSE): masks were displaced on targets whose children carry transforms, the mask region given by x, y, width and height was ignored so content reaching past it was not cut off, and the cached mask image was never dropped on layout, leaving a resized viewport masking with an image rasterized for the old size.
Stopped rebuilding objectBoundingBox gradients on every layout size change in the Layer-Based SVG Engine (LBSE), which used to discard the cached gradient and re-collect its attributes (serializing every animated property back to a string, including gradientTransform) on the next paint. That work is wasted for objectBoundingBox units, whose coordinates resolve against the object bounding box with the userspace transform recomputed on every paint anyway, so only the clients are repainted now, while userSpaceOnUse gradients resolve against the viewport and are still invalidated as before.
Cached the SVG fill and stroke paint server directly on the renderer in the Layer-Based SVG Engine (LBSE), instead of in the shared referenced-resources table living in a renderer's rar data, where every fill and every stroke paid the cost of a hash map lookup just to reach the cache, which gives a small win on the MotionMark/Suits performance test. It also fixed a shape referencing a paint server that does not exist yet, which kept painting unfilled once an element finally took that id, because the shape registered itself as a pending resource under the full resolved URL while the lookups used the bare fragment identifier.
Sped up mapping the paint dirty rect through transforms in the Layer-Based SVG Engine (LBSE). Transformed SVG paints inverted the full 4x4 matrix on every paint, and now use the cheaper inverse of the 2x3 affine transform whenever the transform is affine, falling back to the 4x4 inverse only for 3D transforms.
Made SVG clip-path no longer force a RenderLayer in the Layer-Based SVG Engine (LBSE). A bare clip is now applied during painting through a shared ClipPathPaintScope, a scope object that sets up the clip in its constructor and tears it down afterwards, handling CSS basic-shape and box clips as well as SVG clipper resources so both regular CSS boxes and SVG content share one path. This is a further step in removing the intrinsic need for layers on SVG renderers, continuing the effort to close the performance gap between LBSE and the legacy SVG engine.
Fixed dynamic x and y updates on SVG <foreignObject> elements, which stopped taking effect after the viewport geometry started being derived from the resolved style. The x, y, width and height attributes are presentation attributes mapped to the CSS x, y, width and height properties, but only width and height marked the presentational hint style as dirty when they changed, so a style recalc never ran for x and y and layout kept reading stale values. All four geometry attributes now invalidate the presentational hint style, matching how <rect> handles its geometry, so setting x.baseVal.value from script repositions the <foreignObject> as expected.
Added support for external and data: URL references to clip-path, markers and paint servers (gradients and patterns) in the Layer-Based SVG Engine (LBSE). Until now the LBSE resource resolvers only looked for the referenced fragment inside the local document, so markup like url(file.svg#id) silently resolved to nothing, while filters already worked and the legacy SVG engine handled all of these since a few weeks.
Fixed SVG filters vanishing on elements with a very large bounding box in the Layer-Based SVG Engine (LBSE). The filter region was seeded with the element's object bounding box and then united with each referenced <filter> region, but a referenced <filter> brings its own region, and that region alone decides where the filter paints, so the union could grow far past the image buffer limits and get clamped down to scale that made the output disappear. When every function in the chain is a <filter> reference the bounding box is now dropped and only the referenced regions are kept, matching what the legacy SVG engine does, while objectBoundingBox filter units still resolve exactly as before and HTML/CSS filters are untouched.
Add the WebView::run-color-chooser API to WPE to allow applications to show color choosers, similar to WebKitGTK's API.
The WPE port can now use libsecret for persistent credential storage, reusing the implementation from the WebKitGTK port. This is disabled by default and can be toggled passing -DUSE_LIBSECRET=ON to CMake when configuring the build.
Add the WebKitClipboardPermissionRequest API to WPE, allowing support for the clipboard permission similar to WebKItGTK.
The videos of the Web Engines Hackfest 2026 talks have been published, including the sessions from the new WPE WebKit track. This year the following WebKit-related talks have been recorded:
From libwpe to WPEPlatform: Building Embedded Browser on RPi with New WPE API, by Kate Lee, about new embedding API that is planned to be deemed stable for the 2.54 release series.
Refactoring composition in WPE with Skia, by Carlos García Campos, about the new Skia-based compositor currently in development that has been recently featured in our weekly dispatches.
Techniques for reducing GPU usage in WPE, by Paweł Lampe, which is exactly what reads in the tin and how to balance CPU and GPU usage.
Speeding up WPEWebKit and WebKitGTK with Profile-Guided Optimization, by Lauro Moura, because everybody likes to squeeze performance out of an existing codebase without rewriting it thanks to compiler techniques.
No QA, No WPE: Catch Regressions Before You Do, by Claudio Saavedra and Nikolas Zimmermann, about how continuous integration and quality assurance has been improved (and continues to) to improve stability and performance in the GTK and WPE WebKit ports.
WPEPlatform API for Android, by Alejandro G. Castro, which is not only this year's update on WPE Android but also covers how the platform-dependent components have been partially rewritten and migrated to the WPEPlatform API.
That’s all for this week!
Safari Technology Preview Release 249 is now available for download for macOS Golden Gate and macOS Tahoe. If you already have Safari Technology Preview installed, you can update it in System Settings under General → Software Update.
This release includes WebKit changes between: 316531@main…317820@main.
aria-labelledby relation was not built when its target element was inserted via innerHTML. (316913@main) (179677418)play() on a finite scroll-driven animation timeline did not reset its start time to the current time. (316745@main) (181712713)animation-timeline did not use the last matching timeline when it matched multiple timelines. (317330@main) (182405676)animation-timeline could match a timeline outside the nearest timeline-scope element with that name. (317376@main) (182417407)attr() function. (316672@main) (173992627)outline-offset: inset. (316677@main) (180547604)@function custom functions rule. (316671@main) (181625123)wrap-inside property. (317182@main) (181751564)if() function. (316813@main) (181806268)calc-mix(). (316847@main) (181839626)justify-self on block-level boxes. (317212@main) (181926245)text-decoration-inset. (317230@main) (182019037)content property on the ::marker pseudo-element. (317211@main) (182066124)view-transition-name dynamically did not create a stacking context. (317310@main) (178316558)auto did not correctly compute their minimum content contribution. (316697@main) (181146154)anchor-center in vertical writing modes not being scroll-adjusted along the block axis. (316537@main) (181413103)clip-path shapes could render with incorrect arcs. (317397@main) (182443333)::highlight() and ::target-text ignored text-underline-offset. (317440@main) (182466757)::highlight() decoration replaced the originating element’s decoration instead of layering over it. (317439@main) (182523249)datalist popup menu was presented in the wrong place on a screen to the left of the main screen. (316965@main) (181967418)popover=hint. (317402@main) (129495028)Temporal object. (316742@main) (181723535)box-shadow rendering broke when the page was zoomed. (317080@main) (169167365)scroll-snap-align was not respected when scrolling to an anchor or calling scrollIntoView(). (317495@main) (182544180)HTMLModelElement did not match spec for non-opaque colors. (316788@main) (181641249)closedby attribute for dialog elements. (316680@main) (180105698)DOMMatrix and IntersectionObserver correctly enforcing absolute-length unit requirements when parsing values. (316532@main) (181453666)text/html and text/plain documents. (316976@main) (181763736)replaceChildren() could not replace a document’s children. (316909@main) (181921026)PannerNode orientation-only changes not updating the directional cone gain. (316541@main) (181413407)Layout Invalidated events in Web Inspector to reflect the number of elements affected by a layout invalidation. (317302@main) (181243589)<input> element showed a Scroll badge in the Elements tab. (316773@main) (101661656)@scope with a bare declaration. (316987@main) (181745000)crimson named color to the wrong RGB value. (317047@main) (182090064)0% color stops and misparsed leading radial color stops. (317192@main) (182172439)-apple--apple-system. (317167@main) (182178871)Error stack traces did not include names for modules instantiated with WebAssembly.instantiateStreaming. (316642@main) (181523735)OverconstrainedError to inherit from DOMException and expose a code attribute per the Media Capture spec. (316578@main) (180728516)Update on what happened in WebKit in the week from July 14 to July 27.
This two-week update includes plenty of changes to the Skia compositor, changes to multimedia support, three blog posts, and assorted improvements.
The Web Inspector “Layout & Rendering” timeline now shows a Layout Invalidated event for every element
that needs relayout, not just the layout root (with the old root-only event
renamed to Layout Scheduled). This unveils why some layouts take much longer
than others. No more guessing which of dozens of nodes is actually to blame!
The webkit://gpu page has gained a dark
style, which will be used when the
system settings indicate that dark mode is preferred by the user.
GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.
The experimental GstWebRTC backend was
removed and libwebrtc usage was
enabled in the main branch. We hope
to enable WebRTC support by default in the 2.56 series, scheduled around March
2027.
MP4 edit lists support was enabled in the MSE backend, improving timestamp accuracy, specially when handling of B-frames.
Split the compositing walk in the
Skia compositor into a damage pass and a paint pass, so the frame damage is
known before the first draw. The damage pass walks the layer tree with a
SkNoDrawCanvas in place of the real canvas, so every draw is discarded and
only the damage is collected. Both passes run from a single paint() that
applies animations and computes the transforms once, so the two see the same
tree. Knowing the damage up front is what lets the compositor eventually paint
only the parts of a frame that actually changed.
Wired up damage-driven compositing on
the Skia compositor, so a frame re-composites only the region that actually
changed instead of the whole surface, when the
UseDamagingInformationForCompositing feature is enabled (not yet on by
default). Each frame's damage is combined with what each swap-chain target
still needs to redraw since it was last drawn into, and the clear and every
draw are clipped to that region, which is a milestone towards no longer
repainting untouched pixels every frame.
Made the root layer collect the frame damage itself in the Skia compositor, instead of having each layer report its own changes. Reporting leaves a gap whenever a layer is in no position to report, e.g. a destroyed one took its painted rectangle with it, so what it had drawn stayed on screen. The root now holds one rectangle per layer and compares it against what each frame's walk finds, so a layer that moved is repainted in both places, and a layer the walk never reaches is repainted where it used to be and dropped. Nothing has to notice anything for the pixels it left behind to be repainted, which is what makes it safe to restrict composition to the damaged region by default in future commits.
Limited every content draw to the target's repaint region in the Skia compositor, so a composited frame can redraw only the pixels that actually changed. Each content type restricts itself to the region's rectangles rather than clipping the canvas, since a multi-rectangle clip cannot be a hardware scissor and would make Skia build a mask and break batching. This is the groundwork for damage-driven compositing, which stays off by default behind the damage-tracking feature flag, as the compositor still passes no region and nothing is restricted yet.
Made each swap-chain target track its own damage since it was last current. Repainting only what changed is correct only when drawing into the target that holds the previous frame, but the swap chain hands back whichever target is free, and that one is a frame or more behind. Each frame's damage is now added to every target as it is recorded and cleared from a target when that target is presented, instead of being built as a side effect of reading it.
Taught the tile and image draws in the Skia compositor to split themselves by
damage rectangle, so a frame only repaints
the parts of a layer that actually changed. A new SkiaDamageRegion holds the
frame's damage in device space and is built once per frame, and each draw is
restricted to it: skipped when it touches no damage, split into one sub-draw
per damage rectangle it overlaps, or drawn under a device-space clip when a rotated
or skewed transform rules out working with rectangles. Nothing feeds a damage region
in yet, so every draw still paints in full—this prepares for future patches
enabling using damage information in the composition
Fixed missing repaints when compositor-applied layer state changes dynamically in the Coordinated Graphics backend. A layer recorded damage when its backing store re-rendered or a new contents buffer arrived, but the compositor also handles filters, masks, clip path changes, the contents rectangle, the contents tiling, the blend mode and contents visibility, and changing any of those alters the pixels it produces without dirtying a tile. Those setters now damage the whole layer, so a compositor that repaints only the damaged rectangles no longer leaves the previous frame's pixels on screen.
Nikolas Zimmermann has written a two-part blog series about the current the new Layer-Based SVG Engine (LBSE), with the first post covering the effort to reduce layer overhead using layers conditionally, and the second about how compositing is being implemented and the complications introduced due to paint ordering rules.
Loïc Le Page has published a blog post explaining how to use the new WPEPlatform API to implement a custom WPE integration. While presented example uses GLFW and EGL to show Web content on an X11 window, the concepts are useful for anyone looking into embedding WPE.
That’s all for this week!
Update on what happened in WebKit in the week from June 30 to July 13.
The summer continues with many updates to the new SVG engine (LBSE), improvements to the new Skia-based compositor, some small API additions, and ever-important stable releases with security fixes.
Enabled the CloseWatcher
API and
dialog's closedby attribute in stable.
New API has been added which allows
specifying per-navigation User-Agent string values using
webkit_policy_decision_use_with_policies().
Applications now have more
granularity to decide which User-Agent websites are presented with,
complementing the existing global
WebKitSettings:user-agent
setting.
Roughly halved the cost of the Skia based compositor on WPE running on Vivante GPUs with the Etnaviv driver, by turning off Skia's mipmap sharpening option. That option is enabled by default and makes the Skia shader generator append a small negative level-of-detail (LOD) bias to every mipmap-capable texture sample. WPE does not use mipmapping at all, so the bias sharpened nothing, but it still turned each texture fetch into a LOD lookup, which is a slow path on the tiled GPUs found in the i.MX series. Disabling it restores usage of faster, plain fetch operations.
Fixed broken rendering with the Skia compositor on WPE when super-tiled textures are enabled on Vivante GPUs. Those tile buffers are allocated padded up to a multiple of 64 pixels, so the physical texture is larger than the logical tile, but the Skia backing failed to take this difference into account, leading to distorted tile images being rendered.
Stopped the Skia compositor from blending opaque layers on WPE. Every layer was drawn with the default source-over blend mode, which leaves GPU blending switched on even for fully opaque layers that do not need it, so the cost was paid on every composited frame.
Layers that are opaque, drawn at full opacity and using the default blend mode are now composited with a plain source blend mode instead, which lets Skia turn blending off and lowers GPU bandwidth usage, benefiting tiled GPUs the most.
Cached the concatenated SVG transform attribute
matrix on graphics elements in the
Layer-Based SVG Engine (LBSE).
Reading the transform attribute walked the whole transform list and
multiplied every item together again, and that happened around three times per
animation frame for each element, even though the result only changes when the
transform list itself is mutated.
The concatenated matrix is now stored on the element and invalidated whenever a transform-related attribute changes, so the multiplication runs once per mutation instead of once per read. This cuts repeated matrix work out of the per-frame path for animated SVG content.
Moved the clip out of the SVG child-paint loop in the Layer-Based SVG Engine (LBSE).
Painting a container used to set up a clip rectangle for every child shape in turn, so each shape did its own graphics-context save, clip and restore even though the clip rectangle was identical for all of them. When there is a single region to clip to and no child paints into its own layer, that clip is now established once and shared by every child, transformed or not.
This removes a per-shape save and clip from the hot painting path of SVG documents with many children.
Cached the SVG transform origin on SVG renderers in the Layer-Based SVG Engine (LBSE).
Every transform flush recomputed the origin for each non-layered SVG shape,
even though it only depends on the transform-origin style and the transform
reference box, and sampling MotionMark's Suits test at fixed complexity showed
that computation taking around 1% of the WebProcess main thread.
The origin is now cached and keyed on the reference box, with a style change
to transform-origin or transform-box dropping the cache, and the fast path
is limited to plain SVG transforms so viewport containers and CSS-transformed
renderers keep computing it directly. This removes a repeated per-shape cost
from animated SVG content, and the caching scope can be widened later.
Cached the SVG viewport size used to resolve the transform reference box in the Layer-Based SVG Engine (LBSE).
The default transform-box for SVG is view-box, so every transformed shape
resolved the viewport from the SVG root's content box again on each query,
both when updating its local transform and again during paint. The viewport is
constant after layout, so it is now cached on the <svg> element and only
recomputed when layout actually changes it, on resize, zoom or a viewBox
update. This removes another repeated per-frame computation from the transform
path for animated SVG content.
Coalesced the SVG transform flush into one minimal repaint per container in the Layer-Based SVG Engine (LBSE).
Once per rendering update WebKit processes every SVG renderer whose transform
changed, whether from script or an animation, and that repaint pass was the
dominant per-frame cost on MotionMark's Suits subtest. Instead of walking each
moved renderer up to its repaint container, the flush now computes each
child's rectangle in its parent's coordinate space, unions the children per
parent, maps that single union up the chain once, and issues one
repaintUsingContainer() call per repaint container rather than one per
shape.
This also stops requesting outline bounds, which for SVG merely duplicated the
visual overflow rectangle, and refreshes the bounding-box and visual-overflow
caches that a layout would normally update, so getBBox() and paint or
hit-test culling never read a stale rectangle. This collapses many
backing-store invalidations into one while keeping the repainted region
minimal, closing the performance gap to the legacy SVG engine.
Avoided re-resolving the SVG transform from style on every paint in the Layer-Based SVG Engine (LBSE).
Non-layer SVG renderers already cache their transform in m_localTransform,
but the painting code path used to recompute it from scratch each time,
concatenating the transform list, applying transform-origin and
multiplying matrices, only because the cached value uses a different transform
origin. The paint transform is now derived directly from the cached one by
translating around the nominal origin, which removes that per-paint
recomputation and cuts the cost of painting transformed SVG content.
Fixed a repaint bug in the
Layer-Based SVG Engine (LBSE) where dynamically changing a marker's
markerUnits or orient attribute left stale pixels behind. Such a change
resizes every shape that references the marker, but a referencing shape
without a layer gets no post-layout position update, so only its new bounds
were repainted—a shrinking marker left its former area on screen.
The visual overflow rectangle, markers included, is now cached at the end of shape layout while the geometry is still current, so a marker change can repaint the old bounds before recomputing the new ones. The extra repaint is limited to markers, since gradients and patterns do not affect a client's bounds, and the resulting repaint rects are more accurate than the legacy SVG engine's.
Added a new feature flag,
BackForwardCacheWithMedia, which may be used to disable storing pages with
media content in the back-forward cache. This should solve the problem with
hardware decoders kept occupied on low-end devices in case of caching pages
with media after navigation.
WebKitGTK 2.52.5 and WPE WebKit 2.52.5 have been released, including a number of fixes for security issues, and therefore it is recommended to update. An accompanying security advisory will be published in the coming days. Additionally, these releases include small improvements and Web compatibility improvements.
That’s all for this week!
Update on what happened in WebKit in the week from June 22 to June 29.
After a small break after the Web Engines Hackgest, we're back with another round of updates, this time with a couple of exciting improvements to the SVG engine, a WebRTC fix, and support for WebP images with the toDataURL() API.
Made RenderLayer creation conditional for SVG renderers in the new Layer-Based SVG Engine (LBSE), so a layer is now only created when one is actually needed for intrinsic reasons (3D transforms, opacity, etc.) instead of unconditionally for every renderer. Plain 2D transforms no longer force a layer and are applied directly during painting. This is the groundwork for follow-up patches that remove the intrinsic need for layers when applying clipping, masking and filters to SVG subtrees. It is an important milestone towards reducing the overhead that has been holding back LBSE performance compared to the legacy SVG engine.
Fixed the paint order of non-composited children around composited SVG siblings in the Layer-Based SVG Engine (LBSE). A layered container paints its children from a single flat list in DOM (and SVG paint) order, but some children are composited into their own GraphicsLayer for reasons like will-change, a 3D transform or certain opacity cases. The flat child list is now split into contiguous paint-order segments at those boundaries, with each run of plain children painted by its own overlay layer placed at the correct depth in the compositor's child list. This keeps every child in its DOM order without giving trailing siblings a RenderLayer or backing store of their own, and a container with no composited children produces no segments at all, so the common case costs nothing. This allows us to support composition within LBSE subtrees in a performant way, after dropping the requirement that every renderer creates a layer.
GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.
Fixed initial decoding issues on LibWebRTC on platforms that do video decoding on the final playback stage (for efficiency and performance), instead of on the LibWebRTC decoder component.
Added support for producing WebP images with canvas' .toDataURL(). Using 1.0 as the quality setting will produce lossless images, which matches the behaviour of Chromium and Firefox.
That’s all for this week!