docs

Optimizing Performance

Optimize rendering performance, eliminate layout bottlenecks, profile JavaScript execution, and tune memory settings.

On this page

You can keep UI responsive across desktop apps, games, and embedded devices by eliminating unnecessary engine work.

The techniques below appear in order of impact, starting with the biggest frame-rate wins and finishing with smaller configuration levers.

Avoid Style and Layout Recalculation

Modifying the DOM or updating element styles forces the renderer to recalculate styles, update layout, and repaint the affected area. In an active UI, these recalculations are often the largest per-frame expense, and the cost increases on larger pages.

The remote Web Inspector's timeline records each recalculation and layout pass so you can track them during profiling.

Animate Transform and Opacity with CSS

Animate elements using CSS animations or transitions on transform and opacity instead of updating styles from script.

CSS
.sliding-panel {
  animation: slide-in 0.3s ease-out;
}

@keyframes slide-in {
  from { transform: translateX(-100%); opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}

CSS animations and transitions of transform and opacity avoid per-frame engine work— the renderer advances them without recalculating styles, updating layout, or repainting.

Updating element styles from script forces a style recalculation and a repaint on each frame. Setting transform or layout properties from script also forces a layout pass (modifying opacity skips layout).

You should avoid animating layout properties like width, height, left, or top, since animating geometry in CSS still forces a style recalculation, a layout pass, and a repaint on every frame.

Batch DOM Writes Before Reads

Perform all DOM and style modifications before reading back computed styles or geometry.

C++
// Write everything first...
bar.style.width = dom::StyleValue::Pct(health * 100);
label.textContent = std::to_string(static_cast<int>(health * 100));

// ...then read (one style and layout pass).
dom::DOMRect box = panel.getBoundingClientRect();

Reading element geometry or a computed style immediately after modifying the DOM forces the renderer to process pending style and layout calculations on the spot. Batching modifications together lets the engine resolve layout in a single pass.

When applying large DOM tree modifications, spread the changes across multiple frames. For more on inspecting and setting element properties, see Reading and Writing Styles and Element Geometry and Scrolling.

🚧 Avoid Interleaving Reads and Writes

Alternating between DOM writes and geometry reads forces a synchronous layout pass on every read. This pattern stalls the frame loop and degrades responsiveness.

Profile and Replace Hot JavaScript

When page scripts cause frame drops, profile the page to identify the most expensive functions, then port them to C++ using the DOM API.

Profile the Page

Profile the page before modifying code to locate slow JavaScript and rendering work.

Use the Remote Web Inspector

Start the remote inspector server to profile views from another window or machine.

C++
// Accept inspector connections from this machine only.
renderer->StartRemoteInspectorServer("127.0.0.1", 9222);

To connect, load inspector://127.0.0.1:9222 in another View— the inspecting application implements ViewListener::OnCreateInspectorView() to host the inspector window. You can also run MiniBrowser from the SDK tools/ folder and enter the address in its address bar.

The remote inspector timeline records JavaScript execution with sampled call stacks, style recalculations, layouts, and repaints. JavaScript breakpoints also require the remote inspector.

Remote inspection requires the Plus edition or higher.

Use the Local Web Inspector

You can call View::CreateLocalInspectorView() to inspect DOM elements and styles directly inside the application— see Handling View Events.

The local inspector does not support timelines or JavaScript breakpoints yet. Use the remote Web Inspector instead when profiling performance.

Profile in Chrome DevTools

When a page also runs in Google Chrome, you can profile script execution in the Chrome DevTools Performance panel for a quick look at expensive functions.

Timing characteristics in Chrome differ from Ultralight— confirm any bottlenecks in the remote Web Inspector before changing code.

Profile with Native Tools

To measure entire frames alongside engine layout, painting, and JavaScript execution, profile native code with a system profiler or generate a trace file.

This workflow requires the Pro edition or higher— see Profiling and Tracing.

Port Hot Functions to the DOM API

The DOM API syntax closely mirrors JavaScript, making porting mostly a line-by-line translation. Native C++ executes faster and avoids JavaScript heap allocations.

Here is a page script that resizes health bar elements every frame.

JavaScript
function updateHealthBars(units) {
  const bars = document.querySelectorAll(".health");
  for (let i = 0; i < bars.length; i++)
    bars[i].style.width = units[i].health + "%";
}

You can port the function to C++ and call it directly from the native frame loop.

C++
void UpdateHealthBars(View* view, const std::vector<Unit>& units) {
  dom::Document document(view);
  auto bars = document.querySelectorAll(".health");
  for (size_t i = 0; i < bars.size(); i++)
    bars[i].style.width = dom::StyleValue::Pct(units[i].health);
}

For more on working with DOM elements in C++, see About the DOM API.

Update the Page Directly from Native Code

When building new UI, update the page directly from native code instead of passing data through JavaScript.

Use Data Bindings

Bind page state directly to native data when presenting values in the UI. Modify data on the Context's home thread and call Sync() once per frame. The library synchronizes only the values that changed— see About Data Bindings.

Use the DOM API

Use the DOM API for one-off modifications, measuring elements, and responding to events in native code. For details on manipulating nodes directly from C++, see About the DOM API.

Disable JavaScript

Disable JavaScript when creating a View if the page does not use scripts.

C++
ViewConfig config;
config.enable_javascript = false;

Both data bindings and the DOM API continue to function with JavaScript disabled. For more advice on optimizing resource-constrained platforms, see Embedded and Device UI.

Use DDS Images on the GPU Renderer

Reference .dds images directly in HTML image tags or CSS URLs.

HTML
<img src="minimap-icons.dds">

Block-compressed .dds images upload directly to the GPU in compressed form and decode only when sampled. They use four to eight times less video memory and load faster than PNG images.

This optimization requires the Pro edition or higher and applies only to the GPU renderer. The CPU renderer decodes DDS images into uncompressed memory, providing no memory savings— see Compressed Textures.

If you already hold GPU textures, you can display them directly without copying via Displaying Custom Textures.

Draw Frequent Updates to a Canvas

Draw content that updates every frame (such as minimaps, radars, or animated menus) into an HTML5 <canvas> instead of moving DOM elements.

JavaScript
const ctx = document.querySelector("#minimap").getContext("2d");

function drawMinimap() {
  ctx.clearRect(0, 0, 256, 256);
  for (const unit of units)
    ctx.fillRect(unit.x, unit.y, 4, 4);
  requestAnimationFrame(drawMinimap);
}
requestAnimationFrame(drawMinimap);

Drawing to a canvas updates pixels directly— the renderer repaints the canvas without recalculating styles or updating layout.

Render Canvases on the GPU or CPU

By default, a canvas uses the render path of its View.

When a View uses the GPU renderer, its canvases draw on the GPU. When a View uses the CPU renderer, its canvases draw in software— both paths skip style recalculation and layout.

Read Pixels Back from a Canvas

When the page reads pixels back using getImageData() or toDataURL(), request a software canvas with willReadFrequently.

JavaScript
const ctx = canvas.getContext("2d", { willReadFrequently: true });

Setting willReadFrequently switches that individual canvas to software rendering— drawing for it no longer runs on the GPU.

🚧 Pixel Readbacks on GPU Canvases

On a GPU canvas without willReadFrequently, getImageData() throws and toDataURL() returns data:,. In addition, putImageData() does nothing. For details on supported canvas features, see Supported Web Features.

Defer Garbage Collection to Quiet Moments

The renderer collects JavaScript garbage continuously on background threads. Occasionally, the engine requires a larger collection that runs on the thread calling Renderer::Update(), pausing it briefly while memory reclaims.

When views remain idle without user input, the engine requests a foreground collection through GCListener::OnRequestGC(). Without a listener installed, the collection runs immediately. You know best when a pause will go unnoticed, such as during a loading screen, a pause menu, or an idle period. Installing a GCListener lets you record the request and trigger it later with Renderer::CollectNow(). This interface requires the Pro edition or higher— see Editions and Feature Macros.

Hold the Collection Request

Override OnRequestGC() to remember collection requests instead of running them immediately.

C++
class QuietGC : public GCListener {
 public:
  void OnRequestGC(Renderer* caller, const GCInfo& info,
                   CodeDropMode mode) override {
    // Don't collect now. Remember the request for a quiet moment.
    pending_ = true;
    mode_ = mode;
  }

  // Call from your loop during a loading screen or a pause menu.
  void CollectIfPending(Renderer* renderer) {
    if (pending_) {
      renderer->CollectNow(mode_);
      pending_ = false;
    }
  }

 private:
  bool pending_ = false;
  CodeDropMode mode_ = CodeDropMode::Nothing;
};

Always call Renderer::CollectNow() on the same thread that calls Renderer::Update().

Attach the Listener

Register the listener with the renderer before starting the frame loop.

C++
// The Renderer doesn't own the listener; keep it alive longer.
renderer->set_gc_listener(&quiet_gc);

For more listener callbacks and heap monitoring options, see Managing Memory.

📘 Held Requests Wait for Explicit Collection

A deferred collection never runs until you explicitly call Renderer::CollectNow(). Background collections and collections triggered by memory pressure continue to run in the meantime.

Reduce Painting on Secondary Views

A View repaints only when its content changes— a static page costs nothing to render.

Cap Secondary View Frame Rates

Cap the frame rate of secondary views that remain visible but do not need full refresh rates.

C++
ViewConfig minimap_config;
minimap_config.max_render_fps = 30;
minimap = renderer->CreateView(256, 256, minimap_config, nullptr);

You can also adjust the limit at runtime with View::set_max_render_fps(), passing 0 to remove the cap.

The cap limits CSS animations, smooth scrolling, requestAnimationFrame callbacks, and repaints. Script changes, data bindings, and DOM API calls update on the next capped frame. User input, view resizing, and showing a hidden view force an immediate repaint.

Hide Inactive Views

Hide views that are offscreen, obscured, or closed instead of destroying and recreating them.

C++
menu->set_visible(false);  // skipped by Render(), animations paused

// Later:
menu->set_visible(true);   // resumes, repaints on the next Render()

Hiding a View stops Renderer::Render() from painting it, pauses CSS animations and requestAnimationFrame callbacks, and fires visibilitychange on the page.

JavaScript timers continue running while hidden unless ViewConfig::enable_hidden_timer_throttling was enabled when creating the View, which throttles repeating timers to about once per second— see Updating and Rendering.

Tune Memory Usage

You can control memory usage by selecting an allocation profile and reclaiming cached resources at quiet moments.

Set a Memory Profile

Set Config::memory_profile before creating the renderer to tune heap allocation.

C++
Config config;
config.memory_profile = MemoryProfile::LowMemory;
Platform::instance().set_config(config);  // before Renderer::Create()

Use MemoryProfile::LowMemory on memory-constrained devices to keep heap usage low. When RAM is plentiful, choose MemoryProfile::Performance to favor execution speed.

Reclaim Caches at Transitions

Call Renderer::Recycle() during a loading screen or level transition to purge cached data.

C++
void OnLoadingScreen() {
  renderer->Recycle(RecycleMode::Full);  // thorough, and heavier
}

Internal caches recycle automatically on a timer. Passing RecycleMode::Full forces a thorough cleanup— save this for natural pauses since the call is too heavy to run every frame.

For more on memory management and cache reclamation, see Managing Memory.

Choose the Render Path

If animated or complex UI runs slowly on the CPU renderer, switch to the GPU renderer. The GPU renderer composites layers directly on graphics hardware— see GPU Renderer Overview.

When using the CPU renderer, you can implement a custom Surface to paint directly into native application memory, removing one pixel copy per frame— see Render Surfaces.