You can build UI for a kiosk, an automotive display, a set-top box, or other low-power hardware using the same HTML and CSS you write for the desktop. Ultralight lets you tune its footprint to fit what your device can afford.

> 📘 Framebuffer Tutorial
>
> [Rendering to a Framebuffer](/docs/2.0/rendering-to-a-framebuffer) walks through displaying your first page on a device step by step. This page covers best-practices and links to relevant guides for each subsystem.

## Best-Practices

* **Draw directly to the framebuffer**. Use the CPU renderer to paint directly into your display framebuffer or compositor plane. You can provide raw destination buffers by registering a custom `Surface` with `Platform::set_surface_factory()`— see [Render Surfaces](/docs/2.0/using-a-custom-surface).
* **Use GPU rendering when available**. If the device has a capable GPU with dedicated VRAM, the GPU renderer should be used (along with DDS image assets) for maximum performance.
* **Avoid resizing Views**. Create the View at the display panel's physical pixel dimensions and avoid resizing it.
* **Avoid repainting unchanged Views**. After rendering a View, you should only present when `Surface::dirty_bounds()` is not empty to avoid unnecessary redraws— see [Updating and Rendering](/docs/2.0/updating-and-rendering).
* **Avoid using JavaScript**. Use the DOM or Data Bindings API for updating your UI from native code instead of JavaScript. See below.

## Updating the Page Without JavaScript

The library's native DOM and Data Bindings API work with scripts disabled and should be used when performance is a critical concern.

Use **data bindings** for values and collections that update often, such as sensor readouts, status indicators, or channel lists. You bind a native struct on your thread, modify it freely, and call `Sync()` each tick to push only what changed to the page— see [About Data Bindings](/docs/2.0/about-data-bindings).

Use the **DOM API** to modify document structure, update CSS styles, and listen for events directly from C++.— see [About the DOM API](/docs/2.0/about-the-dom-api).

## Optimizing for Memory

Set `Config::memory_profile = MemoryProfile::LowMemory` before creating the `Renderer`. Under this profile, allocations pack densely, freed heap memory returns to the OS promptly, and garbage collection runs eagerly— see [Managing Memory](/docs/2.0/managing-memory). The profile is read when the `Renderer` is created and by each `View` at its creation.

## Using GPU Acceleration

If your device has a capable GPU, set `ViewConfig::is_accelerated = true` and provide your own `GPUDriver` implementation— see [GPU Renderer Overview](/docs/2.0/gpu-renderer-overview).

Using block-compressed textures for UI images reduces GPU memory usage and avoids CPU decode overhead (Pro edition or higher)— see [Compressed Textures](/docs/2.0/compressed-textures).

## Custom File Loader

To load pages, stylesheets, and images from an archive file or directly from memory, implement a custom `FileSystem` and register it with `Platform::set_file_system()`— see [Custom File System](/docs/2.0/custom-filesystem).

## Bundling Fonts

If the device has no system fonts, provide bundled font files by implementing a custom `FontLoader` and registering it with `Platform::set_font_loader()`— see [Custom Font Loading](/docs/2.0/custom-font-loading).


