Planet Igalia WebKit

September 16, 2026

WPE WebKit Blog

WPE WebKit 2.54 highlights

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 new WPE API

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.

Additions and final adjustments

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.
  • Gamepad rumble support, through 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.
  • A new WPE_SETTING_OVERLAY_SCROLLBARS setting, enabled by default, which may be disabled by applications to opt into classic, always-visible scrollbars.
  • A new 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:

  • The 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.
  • The WPE_DMABUF_BUFFER_FORMAT environment variable has been renamed to WPE_BUFFER_FORMAT.

Beyond Linux: Android

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.

On the WebKit API side

The shared WebKit API has also seen additions this cycle:

Graphics improvements

A new Skia-based compositor

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:

  • Tile contents are recorded into deferred display lists and replayed on the compositor thread, so painting worker threads no longer need to touch the GPU at all.
  • Batched painting groups the drawing of many layers into a single Skia call, which improves performance on pages with many layers that can be painted in the same operation.
  • Unnecessary clip operations are avoided whenever possible, keeping the batched paths effective.

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-aware compositing

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.

The performance impact

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%.

Bar chart comparing MotionMark scores on a Raspberry Pi 4: MotionMark 1.3.1 at 30 FPS goes from 246 with TextureMapper to 334 with the Skia compositor (+36%), and the Composition variant from 131 to 189 (+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.

Bar chart comparing GPU load during the same benchmark runs: 56% with TextureMapper against 39% with the Skia compositor on MotionMark 1.3.1, and 47% against 26% on the Composition variant

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.

Other rendering improvements

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.

Better multimedia on embedded hardware, and a WebRTC transition

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:

  • Hardware video decoding and encoding for platforms with a Qualcomm GPU has been added, leveraging the qtic2vdec and qtic2venc GStreamer elements.
  • Video decoding limits are now respected in MediaCapabilities queries, and can be overridden with the WEBKIT_GST_VIDEO_DECODING_LIMIT environment variable, so a single build can serve devices with different capabilities.
  • Resource usage on pages containing many videos has been improved, by stopping the pipelines of muted, invisible video elements.
  • A new feature flag, enabled by default, allows low-end devices to skip caching pages with multimedia content in the back-forward cache, so that a suspended pipeline cannot hold a scarce hardware decoder hostage.
  • Media capability reporting is more accurate: non-AAC 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.
  • Experimental support for SourceBuffer.changeType() has been added to the MSE backend, along with a fix for playback stalling at ad transitions on Twitch.
  • Persistent licenses are now supported in the Thunder CDM for encrypted media.

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.

WebXR

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.

Other improvements to the WPE port

Several long-standing gaps in the WPE port have been closed this cycle:

  • A built-in popup menu is now used as the default implementation for <select> elements when the WebKitWebView::show-option-menu signal is left unhandled, so option menus work out of the box.
  • Initial drag-and-drop support has been added: web views now handle drags driven by the mouse events they already receive.
  • Spell checking is now supported using the Enchant library, enabled by default, and can be toggled at build time with the ENABLE_SPELLCHECKING CMake option.
  • The on-screen keyboard is no longer shown when an element is focused programmatically; it only appears as a result of user interaction.
  • Initial support for the Pointer Lock API can be enabled at build time with the ENABLE_POINTER_LOCK CMake option.
  • Saving files from the Web Inspector now works in WPE.

Web Platform support

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:

What’s new for WebKit developers?

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.

Looking forward to 2.56

The 2.56 release series will bring even more improvements, and we expect it to be released during the spring of 2027. Until then!

September 16, 2026 12:00 AM

September 14, 2026

Igalia WebKit Team

WebKit Igalia Periodical #77

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.

Cross-Port 🐱

Graphics 🖼️

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.

Community & Events 🤝

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!

by Igalia WebKit Team at September 14, 2026 07:07 PM

September 13, 2026

Hironori Fujii

Asynchronous scrolling for touch events in WPE and WebKitGTK

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.

What is asynchronous scrolling? #

Why it is needed #

A naive implementation of scrolling looks like this:

  1. The UI process receives an input event (wheel or touch).
  2. It sends the event to the web process main thread.
  3. The main thread runs the page’s JavaScript event listeners.
  4. If nothing called preventDefault(), the scroll position is updated.
  5. The page is rendered at the new scroll position.

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.

The scrolling tree and event regions #

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.

Why the iOS implementation could not be reused #

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.

How a touch becomes a scroll in WPE #

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.

The change #

Enabling touch event regions #

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.

UI process: send to the EventDispatcher instead of the main thread #

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.

Web process: classification on the EventDispatcher thread #

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:

  • NotTracking — no listeners. Reply handled = false immediately, without bothering the main thread at all. The UI process can start scrolling right away.
  • Asynchronous — passive listeners only, so nothing can cancel the event. Reply handled = false first so scrolling starts, then deliver the event to the main thread.
  • Synchronous — a non-passive listener may call 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.

Classifying a touch in the scrolling tree #

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.

Keeping the animation running on the scrolling thread #

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.

The event flow, summarized #

Before:

The UI process sends the touch event to the web process main thread, which may
be blocked by JavaScript or layout. Only after the reply arrives does gesture
recognition synthesize a wheel event and scrolling
start.

After (no listeners, or passive listeners only):

The UI process sends the touch event to the EventDispatcher thread of the web
process, which asks the scrolling tree and replies immediately without the main
thread, so gesture recognition synthesizes a wheel event and scrolling starts
right away. If passive listeners exist, the event is also delivered to the main
thread afterwards.

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.

Layout test updates #

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.

Summary #

  • On WPE and GTK both the layer tree and the scrolling tree live in the web process (Coordinated Graphics), so the iOS touch asynchronous scrolling implementation could not be reused directly.
  • Instead, touch events were given the same shape that already works for wheel events: ask the scrolling tree from the EventDispatcher thread.
  • The keys were enabling touch event regions, and making the IPC reply AnyThread so it can be sent without waiting for the main thread.
  • Anywhere the page has no non-passive listener, scrolling now starts regardless of what the main thread is doing.

Acknowledgements #

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.

September 13, 2026 12:00 AM

September 08, 2026

Pawel Lampe

Trying WPE Platform API on Raspberry Pi

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.

Setup #

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.

Preparing the image #

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

Trying the image #

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'

Conclusions #

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.

September 08, 2026 12:00 AM

September 07, 2026

Igalia WebKit Team

WebKit Igalia Periodical #76

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!

Cross-Port 🐱

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.

Graphics 🖼️

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.

A few problems with Offscreen Canvas have been fixed.

Releases 📦️

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!

by Igalia WebKit Team at September 07, 2026 11:43 PM

August 31, 2026

Igalia WebKit Team

WebKit Igalia Periodical #75

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.

Cross-Port 🐱

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.

Graphics 🖼️

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.

Infrastructure 🏗️

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!

by Igalia WebKit Team at August 31, 2026 08:12 PM

August 24, 2026

Igalia WebKit Team

WebKit Igalia Periodical #74

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.

Cross-Port 🐱

Fixed flakiness in the Web Inspector heap snapshot tests.

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.

Multimedia 🎥

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.

Graphics 🖼️

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.

Releases 📦️

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!

by Igalia WebKit Team at August 24, 2026 11:30 PM

August 17, 2026

Igalia WebKit Team

WebKit Igalia Periodical #73

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!

Cross-Port 🐱

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.

Graphics 🖼️

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!

by Igalia WebKit Team at August 17, 2026 07:08 PM

August 10, 2026

Igalia WebKit Team

WebKit Igalia Periodical #72

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!

Cross-Port 🐱

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.

Multimedia 🎥

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.

Graphics 🖼️

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.

WPE WebKit 📟

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.

Community & Events 🤝

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:

That’s all for this week!

by Igalia WebKit Team at August 10, 2026 06:51 PM

July 28, 2026

Igalia WebKit Team

WebKit Igalia Periodical #71

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.

Cross-Port 🐱

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.

Multimedia 🎥

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.

Graphics 🖼️

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.

Community & Events 🤝

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!

by Igalia WebKit Team at July 28, 2026 01:04 AM

July 22, 2026

Nikolas Zimmermann

Implementing compositing in LBSE

Keeping paint order correct with paint order segments

July 22, 2026 12:00 AM

July 14, 2026

Nikolas Zimmermann

Reducing layer overhead in LBSE

Conditional layer creation in the layer based SVG engine

July 14, 2026 12:00 AM

July 13, 2026

Igalia WebKit Team

WebKit Igalia Periodical #70

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.

Cross-Port 🐱

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.

Graphics 🖼️

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.

WPE WebKit 📟

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.

Releases 📦️

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!

by Igalia WebKit Team at July 13, 2026 10:59 PM

June 29, 2026

Igalia WebKit Team

WebKit Igalia Periodical #69

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.

Cross-Port 🐱

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.

Multimedia 🎥

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.

Graphics 🖼️

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!

by Igalia WebKit Team at June 29, 2026 09:00 PM

June 16, 2026

Igalia WebKit Team

WebKit Igalia Periodical #68

Update on what happened in WebKit in the week from June 9 to June 16.

The major highlight this week is the Web Engines Hackfest! Despite it, there are a variety of updates as well, such as various improvements to input handling in WPE WebKit and WebKitGTK, WPE menu rendering changes, and a plethora of other smaller improvements.

Cross-Port 🐱

Input methods may now know whether a field is intended to be used as search input, in which case the WebKitInputMethodContext:input-purpose property will have the value WEBKIT_INPUT_PURPOSE_SEARCH.

Due to GTK not providing an equivalent value for GtkInputPurpose, the default behaviour is to continue mapping search fields to GTK_INPUT_PURPOSE_FREE_FORM as before; but custom input methods may use the new value to detect search inputs. When using WPEPlatform, the value is mapped to WPE_INPUT_PURPOSE_SEARCH, which has been added as well.

Handle selections as part of moveBefore.

Corrected user activation propagation for close watchers.

Invalidate :lang() and :dir() selectors after moveBefore.

Enable Close Watchers in preview.

WPE WebKit 📟

WPE now renders its own popup menus for elements such as select. It supports all styling options the web provides such as colors and fonts. The internal menu can be overriden with the existing WebView::show-option-menu signal. Cog for example still renders its own (with a recent commit).

A colorful context menu

A context menu with a simpler style

A context menu inside an iFrame

A context menu inside a rotated container

Community & Events 🤝

The Web Engines Hackfest started! We had a fantastic first day of talks, and now are heading to breakout sessions. Make sure to check the schedule for sessions that may interest you!

That’s all for this week!

by Igalia WebKit Team at June 16, 2026 10:14 AM

June 08, 2026

Igalia WebKit Team

WebKit Igalia Periodical #67

Update on what happened in WebKit in the week from June 1 to June 8.

Another great week, this time we have a performance improvement implemented in the Skia-based compositor, an excellent writeup about how to investigate and isolate memory leaks in WPE WebKit, a couple of multimedia fixes, and a variety of improvements and fixes across WebKit ports.

Cross-Port 🐱

Implement dialog integration with close watcher.

Implement node iterator and live range pre-remove steps for in-progress moveBefore() implementation.

Fix an early return in CloseWatcher close to align with the spec.

The Web Inspector now shows DOM nodes associated with layout and rendering events in a separate column of layout timeline next to initiator, sizing, and timing information. Hovering over rows in the details table highlights the associated node, and clicking it reveals the node in the "Elements" tab. This makes it easier to match events with specific nodes and helps debugging changes to a web page.

Fix popover light dismiss to account for disabled command buttons.

Multimedia 🎥

GStreamer-based multimedia support for WebKit, including (but not limited to) playback, capture, WebAudio, WebCodecs, and WebRTC.

Fix mediaTime provided with requestVideoFrameCallback in case of captureCanvas as source.

Graphics 🖼️

Batched painting support was implemented in the Skia-based compositor, improving the performance in several cases.

Community & Events 🤝

Pawel Lampe published a blog post where he's presenting and discussing a guide on structured approach to narrowing down and debugging memory leaks within WPE WebKit.

That’s all for this week!

by Igalia WebKit Team at June 08, 2026 08:57 PM

June 02, 2026

Igalia WebKit Team

WebKit Igalia Periodical #66

Update on what happened in WebKit in the week from May 19 to June 1.

The main feature of this week are new releases: stable ones with many security fixes, and development ones with the new Skia-based compositor enabled. Additionally, there was work on Web-facing features, optimizations, spell checking support for the WPE port, and more.

Cross-Port 🐱

WebKit now supports mirroring MathML stretchy operators using the OpenType rtlm feature.

Replaced the CloseWatcherManager's escapeKeyHandler, which will allow other types of close signals to be supported.

Implemented queuing mutation observer records in the work-in-progress moveBefore() implementation.

Implemented popover integration with close watcher.

Fixed popover light dismiss to account for popovertarget on input buttons.

Content filters now create temporary files in the compiled filters directory, which ensures that a file rename can always be used to place them at their final location. This avoids falling back to a regular file copy, which can be slower, when the temporary directory returned by g_get_tmp_dir() (typically /tmp) is in a different volume than the filters' storage path configured for WebKitUserContentFilterStore.

WPE WebKit 📟

Enabled spell checking support in WPE. The existing implementation for the WebKitGTK port, which uses the Enchant library as a backend, was generalized to provide spell checking support in WPE as well. The feature may be toggled at build time using the ENABLE_SPELLCHECK CMake option.

Releases 📦️

WebKitGTK 2.52.4 and WPE WebKit 2.52.4 have been released; they include a number of fixes for security issues, and it is a highly recommended update. The corresponding security advisory, WSA-2026-0003 (GTK, WPE is available as well. The release also includes a number of small improvements and Web compatibility fixes.

Additionally, development releases WebKitGTK 2.53.3 and WPE WebKit 2.53.3 are available since last week. These include a change to use a new Skia-based compositor by default, which is intended to replace TextureMapper once ready. Therefore, bug reports related to website rendering are particularly welcome when using this and subsequent development releases.

Infrastructure 🏗️

The deprecated and un-maintained Flatpak-based SDK was removed. Developers working on the WPE and GTK WebKit ports are encouraged to migrate to the new SDK.

That’s all for this week!

by Igalia WebKit Team at June 02, 2026 12:12 AM

Pawel Lampe

WPE memory leak investigation playbook

Depending on the web application, the WPE WebKit memory usage trend can vary. When simple web applications are being processed, the memory consumption tends to be virtually stable (the same) no matter the period. However, when more complicated web applications are being executed, the memory usage usually grows over time while going back to normal from time to time e.g., when GC / memory pressure mechanism releases all kinds of caches and not-needed memory. Therefore, memory growth itself is not unusual. Nevertheless, as the memory leaks happen in WPE at times, the memory growth is worth investigating — especially if very rapid or unbounded.

This article presents a structured playbook for investigating such a memory growth and memory leaks in WPE. Rather than diving straight into debugging tools, it starts from first principles: confirming the problem is real, choosing the right environment to work in, and narrowing down the leaking area before any heavy tooling is involved. The goal is to reach actual debugging as fast as possible, regardless of whether the environment is an embedded device or a desktop machine, and regardless of how quickly the problem reproduces.

Playbook #

The high-level list of recommended steps to follow is presented below. In a nutshell, the steps 1, 2, and 3 are meant to choose and follow the fastest possible investigation path so that actual debugging of the problem (step 4) can be started as soon as possible.

  1. Confirming the problem
  2. Identifying the best setup for reproducing the problem
  3. Narrowing down
  4. Debugging

1. Confirming the problem #

The ultimate first step when working with alleged memory leak is to check whether the observed memory growth is actually abnormal. In the case of web browsers in general, the memory growth alone may not necessarily mean something is leaking. There may be many regular reasons why the browser’s memory usage is growing, but the usual suspects are:

  • JavaScript-level memory allocations — due to the very nature of JavaScript, the memory it allocates causes the overall web content process memory growth up until the garbage collector (GC) kicks in. Then (from the RSS perspective) some memory is usually freed. However, as it’s not easy to predict when the GC will be invoked (e.g., when the browser processes an application that performs heavy rendering), it’s possible that memory will grow but remain garbage-collectible.
  • JavaScript Just-in-Time (JIT) compilation — when not explicitly disabled or limited, the processing of any web application that has JavaScript code associated with it will cause the browser to continuously compile the JavaScript code in the background so that it executes such code faster in runtime at the expense of memory that is required for storing compiled artifacts.
  • Caches — as the WPE operates, it caches things such as web resources, style resolution artifacts, textures, glyph atlases, layer tiles, display lists, rasterization artifacts, and many others. Naturally, the cache sizes are limited, however, if many caches are growing at the same time, they may create an impression of a leak. The difference in that case is, the caches stop growing at some point.

Due to the above, to confirm the memory growth is abnormal, one should usually try the following first:

  1. Triggering memory pressure to force the browser to trigger GC and evict as many cache entries as possible,
  2. Rerunning the browser with JIT disabled to rule out the JIT-related memory growth — unless the application code is very small.

If the memory growth doesn’t stop with JIT disabled or its level does not go back to normal after triggering memory pressure, the growth can be assumed to be abnormal, and one can proceed to the next step.

2. Identifying the best setup for reproducing the problem #

When the memory growth is atypical, it needs to be narrowed down in a way that the final debugging is possible. For both narrowing down and the debugging, one should aim at the most flexible development environment along with the smallest possible web application that reproduces the problem quickly. What it means in practice is — desktop environment along with small demo web application that reproduces the problem. Whilst it’s not always possible to have such an environment, the 3 general rules are as follows:

  1. Desktop environment is usually better than embedded one in terms of working with memory leaks as it offers minimal overhead (e.g., in terms of compilation times) and huge flexibility in choosing the industry standard tools for profiling/debugging.
  2. Small web application is always better than a big one as long as it still reproduces the same problem in the same amount of time. In such case, a small application minimizes the amount of noise that usually stands in the way of profiling/debugging.
  3. A web application that reproduces the problem quickly is always better than the one that needs much more time for it. The worst thing that can happen in the case of narrowing down memory leaks, is when the memory growth is noticeable or starts after a very long time such as hours/days+.

Given the above, at this point one should go through the below steps:

  1. Check if the setup is trivial enough already — if the web application reproduces the problem quickly in a desktop environment and is simple enough, one should immediately jump to the Debugging section.
  2. Check if the problem can be reproduced on desktop assuming it originally reproduces on embedded.
  3. Check if the problem can be reproduced faster if it’s not reproducing fast enough.
  4. Check if the web application could be simplified.

Once the setup is simplified as much as possible, one should proceed to one of narrowing down sections depending on the setup. Also, if the setup is still not ideal, one should actively seek opportunities for simplifying the setup even during narrowing down as it’s likely that some new information will eventually open new possibilities in terms of simplifying setup.

3. Narrowing down #

When the problem has been confirmed but there are not enough clues to tell exactly which parts leak, the debugging cannot be started right away. In such case, it’s necessary to narrow down the problem to the browser/application area that can be easily debugged.

While in some cases narrowing down is not even necessary, quite often it takes orders of magnitude more time than actual debugging, and hence one should pay special attention to this step.

3a. Narrowing down on embedded when the problem takes a long time to reproduce #

This is the toughest situation one can find themselves in. When a problem takes a long time to reproduce (hours/days+), every iteration/test comes automatically with a significant cost. Moreover, when the environment is an embedded one, rebuilding WPE is usually more time-consuming and the amount of tooling is usually limited — or requires some work to bring it to the image at least.

Due to the above, narrowing down the problem in this setup requires a structured approach with extra care. In such case, the things to check should be approached in steps defined as follows:

  1. Things to check without rerunning the WebKit
    • in case of embedded devices, extra care is needed when attaching a memory profiler. On low-end devices, memory profilers tend to slow down the application hard enough to trigger otherwise non-existent problems.
  2. Things to check without rebuilding the WebKit
    • in case of embedded devices, one should prefer limiting JIT over disabling it as without it, the JS execution may be slow enough to trigger unexpected scenarios.
  3. Things to check if rebuilding WebKit

Ideally, while checking various things along the above steps, one should batch as many checks as possible within individual tests.

3b. Narrowing down on embedded when the problem reproduces quickly #

When the problem reproduces quickly, the limitations of embedded environment are not that relevant. In this scenario, one should prioritize getting debug symbols (RelWithDebInfo build) into the image and utilizing them by running the browser with whatever profilers are available. For the specific things to check, one should seek inspiration in the following groups:

  1. Things to check without rebuilding the WebKit.
  2. Things to check if rebuilding WebKit.

3c. Narrowing down on desktop when the problem takes a long time to reproduce #

This situation is similar to 3a and hence one should follow the things to check from the following groups:

  1. Things to check without rerunning the WebKit.
  2. Things to check without rebuilding the WebKit.
  3. Things to check if rebuilding WebKit.

However, this time, there are some extra opportunities around tooling:

  1. There should be many more tools available already in the system or available to be installed.
  2. Tools such as memory profilers that could slow down the application making it unusable on embedded, may turn out to be working well when the desktop-class processing power is available.

With the above in mind, it’s worth trying all the tools available with priority because if at least one tool works well, one can save hours of narrowing down.

3d. Narrowing down on desktop when the problem reproduces quickly #

This is technically the simplest possible scenario, so basically, all the possibilities are available. The most time-consuming activity in this case is very likely rebuilding WebKit itself — although it should still be relatively fast. In such case, just after a few quick checks with the Web Inspector, it’s recommended to get debug symbols (RelWithDebInfo build) and start with tools such as memory profilers.

Other than the above, one should go through the following groups on things to check:

  1. Things to check without rebuilding the WebKit.
  2. Things to check if rebuilding WebKit.

4. Debugging #

The WPE debugging is twofold and depends on whether the problem is within the engine (usually C/C++ code) or the web application (JavaScript code).

When problem lies in the engine #

Debugging WPE WebKit is the same as debugging any other C/C++ application on Linux (or Mac if the issue is cross-port and one prefers an Apple port to work with), and hence is outside the scope of this article. Some WebKit-specific information can be found in the WebKit Documentation article on building and debugging page and therefore is recommended as a first step.

When problem lies in web application #

When the problem lies in JavaScript code, the situation is usually fairly straightforward. The majority of bugs in this area should be reproducible across various browser engines and hence a full variety of tooling should be available. If the WebKit is preferred or if the problem reproduces only there, the tooling available is still very useful and helps debugging problems quickly. The ultimate tool in such case is the Web Inspector. On official WebKit’s web page there’s entire index of articles on Web Inspector. Among those, the most interesting read is about Timelines Tab where the most useful debugging can be done. Once the features of Timelines Tab are understood, the next important article is the memory debugging guide. It dives into the most important Timelines Tab subsections and showcases the work with heap snapshots which is a key. To supplement it, it’s very important to know the heap snapshot delta feature which is basically about button:

Web Inspector heap delta.

that allows one to inspect the delta-snapshot between 2 snapshots. It’s critical as it answers the question on what JS objects were added between the base snapshot and the later one. If some objects are piling up, it immediately shows which ones.

One important note on snapshots is that in some cases when using Web Inspector is not possible, one can generate the snapshots manually from the web engine’s C++ code by just calling GarbageCollectionController::singleton().dumpHeap(); at some appropriate moment. In this case, the dump will be written to standard output. It can be then turned into a file and imported from any Web Inspector using Import button.

As the Timelines Tab with its subsections should be able to answer on what happens, to understand why it actually happens, the last missing piece is the JS debugger within Web Inspector. It’s not very different to debuggers in other engines, but it’s worth checking a dedicated article on it just to understand the capabilities.

Appendix #

Things to check without rerunning the webkit #

Even if the WPE is running with default settings in release mode, there are plenty of useful things that can be checked while the browser is still running:

  1. Identifying which WebKit process allocates abnormally,
    • there are multiple ways to do this, but usually it’s as easy as using ps utility.
  2. Identifying how fast the process in question allocates the memory,
    • this is useful to know at least for comparison purposes, but it may hint some problems already if the numbers correlate with what web application does.
  3. Checking logs from stdout, stderr, and journal (using journalctl).
  4. Checking detailed process memory statistics.
  5. Triggering and checking the impact of memory pressure on given processes RSS,
    • in short, memory pressure triggers the cleanup of the majority of caches along with GC. Therefore, if this is able to bring memory back to normal level, then the problem is about caches, JS Heap / GC, or fragmentation.
  6. Attaching memory profilers if available,
    • even if the debug symbols are not present, this may be useful to see what data is being captured and how the web application behaves when slowed down by profiler.
  7. Attaching other tools if available,
    • even if the debug symbols are not present, various tools offer different perspectives on what the browser is doing. In some cases, such information may reveal some anomalies that may be related to the main issue.
  8. Cross-checking with other browsers,
    • if other browsers show a similar pattern of memory usage, it’s very likely the problem lies in web application itself. Otherwise, it strongly suggests a bug in the WPE.
  9. Cross-checking with other ports,
    • if any other WebKit port shows a similar pattern of memory usage, it allows one to narrow down the area in the code a bit based on what port it is:
      • if the same behavior is visible in any of Apple ports, the problem is most likely related to cross-platform code,
      • if the same behavior is visible only in GTK port, then the problem is most likely related to GLib-related part, coordinated graphics part, GStreamer-related part, or others that are shared.

Things to check without rebuilding the webkit #

  1. Tweaking and checking the logs from WPE,
    • while generic logs may hint some unusual behavior, more specific ones such as GC logs (JSC_logGC=1) may be used to check how the individual JS heap sizes evolve over time and how GC behaves. If it’s JavaScript leaking the memory, this log will quickly provide the evidence.
  2. Enabling Remote Web Inspector and checking:
    • both breakdown and trend of memory usage in the memory timeline after doing a bit of recording,
    • the effects of takeHeapSnapshot() invoked from JS console:
      • as this function usually triggers GC internally, it may be used to check how much RSS memory is reclaimed by GC in isolation (followed up by scavenger),
      • as this function takes a JS heap snapshot, it then can be used to explore manually if its contents point towards something interesting.
  3. Disabling JIT and checking the memory usage,
    • if the memory usage is stable with JIT disabled, one should proceed to the step below.
  4. Limiting JIT and checking the memory usage,
    • there are at least a few places (levels) where JIT compilation engine allocates memory. If limiting doesn’t resolve the issue completely, it’s likely the engine itself leaks some memory around temporary helper-heaps such as AssemblerData etc.
  5. Experimenting with environment variables and runtime preferences,
    • some environment variables and runtime preferences change the behavior of the web engine significantly. If changing one of them makes the problem go away, it usually helps to narrow down the problematic area quickly.
  6. Running WPE with system malloc (environment variable Malloc=1) and checking the memory usage,
    • when one suspects bmalloc/libpas issues with fragmentation or scavenger, it’s worth running a browser with system malloc to compare the memory evolution over time against the bmalloc/libpas.
  7. Limiting device memory and checking the memory usage,
    • if triggering memory pressure is not possible, an alternative solution is to limit the device memory so that the browser is under constant memory pressure.
  8. Running WPE with sysprof and checking:
    • stack traces — to see what parts of engine are particularly active as it may hint some problematic area,
    • WebKit marks — to see what the engine is doing as well as quantitative data in marks such as EventLoopRun etc. as in those cases the numeric value trends may reveal resource pile up.

Things to check if rebuilding webkit #

  1. Building WPE in release mode with debug symbols and re-trying memory profilers or other tools if the debug symbols were not present before,
    • if some desired tools such as heaptrack, valgrind, perf, or strace were not available before, it’s the right moment to get/build them as well,
    • once the debug symbols are in, one should try:
  2. Building and running with Google perftools,
    • as WPE allows switching to system malloc as an allocator, it’s possible to use custom malloc implementation with instrumentation such as gperftools. For that, the recommended read is this article from fellow Igalian, Pablo Saavedra.
  3. Building and running with sanitizers,
    • if the problem is about low-level leak, address/leak sanitizer should be able to help pointing out the problematic area.
  4. Building and running with memory sampler,
    • the data produced by memory sampler is roughly the same as inspector’s memory timeline, however, it’s much more convenient as it doesn’t need web inspector at all.
  5. Building and running with node statistics,
    • when memory growth seems to be related to DOM mutations, it’s worth enabling and reporting node statistics periodically — in some cases, it may directly suggest what the problem is about.
  6. Building and running with malloc heap breakdown,
    • when all other means fail, a very good last-resort approach for investigating memory usage statistics via a debug-only WebKit feature called Malloc Heap Breakdown. The details can be found in the dedicated article about it.
  7. Building and running with libpas statistics,
    • On very rare occasions such as memory fragmentation or allocation issues, it may be worth checking the libpas (low-level memory allocation and management library) statistics as WPE uses it by default on the vast majority of platforms.

Individual instructions #

Checking detailed process memory statistics #

As WPE WebKit uses multi-process architecture, there are multiple processes that can be checked, although the most interesting one is usually the Web Content Process. Once the PID of the given process is determined (e.g., using ps utility) the usual steps to check detailed memory statistics are:

  • cat /proc/<PID>/status or cat /proc/<PID>/statm for very basic statistics,
  • pmap -X <PID> - for detailed statistics (if available),
  • cat /proc/<PID>/smaps_rollup and cat /proc/<PID>/smaps for detailed statistics (requires CONFIG_PROC_PAGE_MONITOR kernel configuration option).

Triggering memory pressure from OS #

WPE uses a so-called Memory Pressure Monitor to observe the memory usage in the system and to react if there’s not much memory left. The default thresholds are specified in MemoryPressureMonitor.cpp and usually are 90% for non-critical and 95% for critical response. Depending on the response, WPE schedules GC and clears internal caches immediately.

As the above is usually on by default, one can leverage it to trigger GC (along with cache cleanups) by filling up the available memory in the OS to 95+%. There are many ways to allocate memory, yet the simplest is using stress:

  • e.g. stress --vm 1 --vm-bytes 1024M --vm-keep to allocate 1024 MB.

Attaching memory profilers #

When attaching any memory profiler, unless one wants to profile only native allocations (Skia, GStreamer, ICU, etc.), the key is to use Malloc=1 environment variable on WPE startup so that bmalloc uses system malloc instead of libpas. Also, if WebKit is using a sanboxed mode in given configuration, it’s usually necessary to use WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 as well. Then the commands are as follows:

  • to attach heaptrack:
    • heaptrack -p <PID> so e.g. heaptrack -p $(pgrep WPEWebProcess) (see this article for details),
  • to run with valgrind’s massif (as attaching to running process is not possible):
    • valgrind --tool=massif --trace-children=yes <WPE-BROWSER-COMMAND> (see this article for details).

Attaching other tools #

If memory profilers are unusable or unavailable, it’s worth checking if other tools are present and experimenting a bit with them if so. In some cases, tools other than memory profilers may give some hints on further investigation or reveal a suspicious pattern within application execution. Some ideas for experiments with various tools are listed below:

  • strace:
    • strace -c -p $(pgrep WPEWebProcess)strace called with -c gives a nice summary of system calls executed by the traced application. It can be useful to check the overall syscall usage pattern to see if there are any anomalies.
    • strace -p $(pgrep WPEWebProcess) -e trace=mmap,munmap,mremap,madvise -ttstrace focused on mmap()-related system calls may be useful to debug libpas.
  • perf:
    • perf record -F 999 -ag -p $(pgrep WPEWebProcess) -- sleep 60 — regular recording with perf can be very useful, especially if symbols are available. With that, one can generate flamegraphs and investigate what’s going on in the browser. While it’s not about profiling memory, it may be helpful to narrow down at least a bit.
    • perf record -F 999 -e syscalls:sys_enter_mmap,syscalls:sys_enter_munmap,syscalls:sys_enter_mremap:sys_enter_madvise -ag -p $(pgrep WPEWebProcess) -- sleep 60perf focused on mmap()-related system calls is much more superior than e.g. strace as it also records stack traces. Therefore, if debug symbols are present, and if the memory growth is very rapid, it’s very likely the libpas mmap() stacktraces will lead to the growth origin statistically.
    • perf trace -e mmap,munmap,mremap,madvise -p $(pgrep WPEWebProcess) — this is very much similar to strace focused on mmap()-related system calls as it shows a live preview of what’s happening.
  • sysprof:
    • sysprof-cli -f — while running system-wide sysprof won’t make WPE push marks into it, the profiling trace may still be useful to some degree, especially if debug symbols are available.

Disabling JIT #

This can be done using an environment variable:

  • JSC_useJIT=false.

Limiting JIT #

Limiting JIT can be achieved via environment variables:

  • JSC_jitMemoryReservationSize=<BYTES> to limit JIT memory usage (the limit is semi-strict as some JIT compilation engine buffers are limited by this value indirectly),
  • JSC_useFTLJIT=false to disable FTL tier,
  • JSC_useDFGJIT=false to disable DFG and FTL tiers,
  • JSC_useBaselineJIT=false to disable Baseline, DFG, and FTL tiers.

Tweaking WPE logs #

WPE is a fairly complex piece of software and hence it offers various logging capabilities related to WebKit itself, as well as to related libraries. The vast majority of logging can be controlled via environment variables:

  • WEBKIT_DEBUG=all to enable all logging channels,
  • WEBKIT_DEBUG=Layout,Media=debug,Events=debug to enable selected logging channels,
  • JSC_logGC=2 to enable JS garbage collector logs,
  • GST_DEBUG=4 to enable gstreamer (multimedia-related) logs (see the documentation),
  • G_MESSAGES_DEBUG=all to enable GLib-level logs.

If MiniBrowser (or similar browser) is used, one can also set a runtime preference to enable JS console.log(...) logging to the standard output:

  • --features=+LogsPageMessagesToSystemConsole.

Enabling remote web inspector #

Enabling WPE’s remote web inspector is a twofold process:

  1. The first step is to run WPE with the proper environment variable so that it starts listening on IP:PORT using tcp socket:
  • WEBKIT_INSPECTOR_SERVER=IP:PORT is the most reasonable option as it uses inspector:// protocol that can be utilized by WebKit-native browsers such as GNOME Web (Epiphany) or Safari,
  • WEBKIT_INSPECTOR_HTTP_SERVER=IP:PORT is a less preferable alternative that uses HTTP protocol and technically works from any browser. However, no seamless integration is guaranteed in this case.
  1. The second step is to connect from a regular web browser to the WPE:
  • using inspector://IP:PORT/ if native inspector server was started,
  • using http://IP:PORT/ if HTTP inspector server was started,
  • forwarding the ports using socat tcp-l:PORT,fork,reuseaddr tcp:IP:PORT if the WPE is running in unreachable network.

Experimenting with environment variables and runtime preferences #

The most outstanding environment variables changing the behavior of WPE are the following:

  • WPE_DISPLAY — assuming the new WPE platform API is used, this environment variable allows one to switch the pre-defined platform implementation thus changing a platform-facing part of graphics pipeline. The valid options are:
    • WPE_DISPLAY=wpe-display-headless — for headless implementation,
    • WPE_DISPLAY=wpe-display-drm — for direct rendering manager integration,
    • WPE_DISPLAY=wpe-display-wayland — for wayland integration,
  • WEBKIT_SKIA_ENABLE_CPU_RENDERING — when set to 1, rendering the DOM contents to the layers is done using Skia CPU backend instead of GPU one.

The most outstanding runtime preferences changing the behavior of WPE are the following:

  • CanvasUsesAcceleratedDrawing — when disabled, 2D canvas will use Skia CPU backend instead of GPU one,
  • LayerBasedSVGEngine — when enabled, WPE uses a different SVG engine internally,
  • AcceleratedCompositing — when disabled, WPE uses experimental, non-composited mode that bypasses all of the compositor work.

Limiting device memory #

On the majority of embedded devices, the device memory can be limited by:

  1. Interrupting the boot sequence (usually holding some key such as z upon booting),
  2. Invoking the command to change the limit and booting, e.g.:
    > global linux.bootargs.console="console=ttymxc0,115200n8 mem=2G"
    > boot
    

Running WPE with sysprof #

Regardless of whether it’s done on desktop (using wkdev-sdk) or on embedded device, the command is always as simple as:

  • sysprof-cli -f -- <WPE-INVOCATION>.

See the documentation entry for more details.

Building WPE in release mode with debug symbols #

On desktop, the simplest way to get release with debug symbols is to utilize CMake’s build type by using -DCMAKE_BUILD_TYPE=RelWithDebInfo within WPE build command, so:

  • ./Tools/Scripts/build-webkit --wpe --release --cmakeargs="-DCMAKE_BUILD_TYPE=RelWithDebInfo".

On embedded, when Yocto is used, one should tweak settings such as:

IMAGE_GEN_DEBUGFS = "1"                                                         
IMAGE_FSTYPES_DEBUGFS = "tar.bz2"
DEBUG_BUILD = "1"
EXTRA_IMAGE_FEATURES_append = " dbg-pkgs"

and potentially INHIBIT_PACKAGE_STRIP to control whether debug symbols should be kept with the binary or not. This may be necessary occasionally as some tools have problems reading .gnu_debuglink and therefore work only with symbols included in the binaries.

Building and running with sanitizers #

WebKit works pretty well with all kinds of sanitizers. To build with any of them a CMake-level helper called ENABLE_SANITIZERS can be used by specifying -DENABLE_SANITIZERS=address, -DENABLE_SANITIZERS=leak etc. With that, the command for building e.g. on desktop could look like:

  • ./Tools/Scripts/build-webkit --wpe --debug --cmakeargs=-DENABLE_SANITIZERS=address.

For more details, one can refer to this article from fellow Igalian, Fujii.

Building and running with memory sampler #

When WPE is built with -DENABLE_MEMORY_SAMPLER=ON, the simple memory sampler can be started along with the browser using environment variable:

  • WEBKIT_SAMPLE_MEMORY=1 (accompanied by WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 if needed).

With that, the memory of various WPE processes is sampled every second, and saved to the files under /tmp directory continuously.

Building and running with node statistics #

Node statistics are a debug-only feature that can be enabled by:

  • changing 0 of #define DUMP_NODE_STATISTICS 0 to 1 in Source/WebCore/dom/Element.h,
  • adding dumpStatistics() call, to e.g. Node constructor in Source/WebCore/dom/Node.cpp.

Building and running with libpas statistics #

Libpas statistics are a debug-only feature that can be enabled by changing 0 of #define PAS_ENABLE_STATS 0 to 1 in Source/bmalloc/libpas/src/libpas/pas_config.h and then running WPE with environment variable PAS_STATS_ENABLE=1.

June 02, 2026 12:00 AM