BlinkGTK Best Practices

Author: BlinkGTK Project
Last updated: 2026-05-12
Audience: application developers building on BlinkGTK


Introduction

This document collects implementation patterns learned from building applications on BlinkGTK. Read it
alongside the BlinkGTK API reference (docs/04-user-guides/api-reference/).

It incorporates suggestions from developers who build e-book and read-aloud (EPUB / DAISY) applications on BlinkGTK.


1. Event-driven patterns with zero fixed delays

1.1 Anti-pattern: DOM manipulation after a fixed delay

Deferring post-load work — setting zoom, applying an initial highlight, adjusting scroll position — with
g_timeout_add() "to be safe" is not recommended.

// Anti-pattern (fixed delay)
g_timeout_add(400, deferred_zoom_and_highlight_cb, self);

Problems:

  1. There are paths where scrollIntoView runs before the DOM is complete — this can produce a
    stream of GTK gtk_widget_compute_point assertions
  2. The delay may be too short depending on environment and load — behavior becomes unstable
  3. The application may enter its shutting-down state during the delay — risk of use-after-free

Catch BLINK_LOAD_FINISHED from BlinkWebView's load-changed signal and run the work synchronously.

g_signal_connect(web_view, "load-changed",
                 G_CALLBACK(on_load_changed), self);

static void on_load_changed(BlinkWebView* web_view,
                            BlinkLoadEvent ev,
                            gpointer user_data) {
    if (ev != BLINK_LOAD_FINISHED) return;
    MyAppView* self = MY_APP_VIEW(user_data);
    if (self->shutting_down) return;

    apply_zoom_and_pending_highlight(self);
    // consume queued work such as autoplay chains here as well
}

Because both hold, it is safe to issue scrollIntoView and execute_javascript calls at this point.
Removing fixed delays has been observed to markedly reduce post-load assertion failures.

1.4 The same applies to other subsystems, such as GStreamer

Waiting for GStreamer preroll follows the same idea: replace fixed delays with an implementation that
watches both ASYNC_DONE and STATE_CHANGED → PAUSED and then performs the deferred seek. Preroll
completion is then detected with no fixed delay at all.

Applying "events, not timers" consistently across the application substantially reduces
timing-related bugs.


2.1 Anti-pattern: injecting <style> with JavaScript on every page

// Anti-pattern (JS injection per page)
static void on_load_changed(BlinkWebView* view, BlinkLoadEvent ev, ...) {
    if (ev != BLINK_LOAD_FINISHED) return;
    blink_web_view_execute_javascript(view,
        "var s = document.createElement('style');"
        "s.textContent = '...';"
        "document.head.appendChild(s);",
        NULL);
}

Problems:

  1. An IPC call (execute_javascript) on every navigation
  2. Risk of hitting the gtk_widget_compute_point assertion during JS-driven DOM manipulation
  3. A brief "unstyled" flash caused by JS timing

blink_web_view_inject_user_stylesheet() (since v1.0.0) automatically re-injects
after navigation, so a single call at startup applies to every page.

// At startup (called once, after BLINK_LOAD_FINISHED)
static const gchar* CUSTOM_CSS =
    "body, p, span, div, ruby {"
    "  font-family: 'Noto Serif CJK JP', serif !important;"
    "}"
    "*[style*='vertical'], .vertical-rl {"
    "  font-feature-settings: 'vert' 1, 'vrt2' 1;"
    "}";

blink_web_view_inject_user_stylesheet(BLINK_WEB_VIEW(web_view),
                                      CUSTOM_CSS);

2.3 Effect

Aspect Anti-pattern Recommended
IPC calls on navigation Every time None (automatic re-injection)
Coverage of vertical-writing font features Misses depending on timing 100%
Risk from JS DOM manipulation Present (the DOM is rewritten while the page is being switched) None
Momentary "unstyled" flash Possible Does not occur

This is especially valuable for content made of many XHTML files, such as EPUB and DAISY.


3. Self-contained portable bundles

How to ship an application that includes the BlinkGTK runtime (roughly 617 MB: 513 .so files plus data
files) as a portable bundle that runs wherever the user unpacks it.

3.1 Distribution layout

my-app-X.Y.Z/
├── bin/
│   └── my-app                  (rpath = $ORIGIN/../lib)
├── lib/
│   ├── libblinkgtk.so
│   ├── libblink_*.so (513 files)
│   ├── icudtl.dat
│   ├── content_shell.pak
│   ├── snapshot_blob.bin
│   └── v8_context_snapshot.bin
├── share/my-app/
│   └── resources/
├── my-app.sh                   (launcher wrapper)
├── README.txt
└── VERSION

3.2 rpath settings for the binary (Makefile)

BLINKGTK_LIBS = -L$(BLINKGTK_REAL_LIBDIR) \
                -Wl,-rpath,'$$ORIGIN/../lib' \
                -Wl,-rpath,$(BLINKGTK_REAL_LIBDIR) \
                -Wl,-rpath-link,$(BLINKGTK_REAL_LIBDIR) \
                -lblinkgtk

$ORIGIN/../lib resolves libraries relative to the binary's own location, so the bundle works no
matter where the user unpacks it. Keeping the absolute-path rpath as a fallback lets make run work
during development, which improves the developer experience.

3.3 Launcher wrapper

#!/bin/sh
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
export LD_LIBRARY_PATH="$APP_DIR/lib:$LD_LIBRARY_PATH"
export GDK_BACKEND="${GDK_BACKEND:-wayland}"
export MYAPP_DATA_DIR="$APP_DIR/share/my-app"
cd "$APP_DIR/share/my-app" || exit 1
exec "$APP_DIR/bin/my-app" "$@"

3.4 Resource-path resolution helper

gchar* my_app_resolve_resource(const char* relative) {
    const char* base = g_getenv("MYAPP_DATA_DIR");
    if (base && *base) {
        return g_build_filename(base, relative, NULL);
    }
    return g_strdup(relative);  // fall back to CWD during development
}

Having both paths is convenient: development builds (make run) work relative to the CWD with no
environment variable, while the bundle launcher sets MYAPP_DATA_DIR so the app reads
share/my-app/resources/... from inside the bundle.

3.5 Distribution size and verification

As one data point: about 220 MB compressed as tar.gz, about 650 MB unpacked.

tar -xzf my-app-X.Y.Z-linux-x64.tar.gz
cd my-app-X.Y.Z/
./my-app.sh

System dependencies are limited to OS-standard components such as GTK4, GStreamer, and libxml2; running
on Fedora 44 with no additional installation has been reported.


4. Common principles: "events over timers", "C API over JS DOM manipulation"

All three patterns in this document embody the same principles:

  1. Event-driven signals rather than timers (g_timeout_add)
  2. BlinkGTK's C API rather than DOM manipulation through JavaScript
  3. $ORIGIN plus environment variables rather than hard-coded absolute paths

BlinkGTK is designed as a GTK4-native GObject type with a full complement of signals, properties, and C
API. Using them keeps application code short and stable.


API Purpose Pattern
blink_web_view_new() Create the widget §1, at startup
load-changed signal Detect DOM completion §1.2
BLINK_LOAD_FINISHED event Determine the completion moment §1.2
blink_web_view_inject_user_stylesheet() Persistent CSS injection §2.2
blink_web_view_execute_javascript() Run JS (only when necessary) §2.1 (anti-pattern)
blink_web_view_load_uri() Load a URL §1

See docs/04-user-guides/api-reference/ for details.


6. Patterns to be added

This document continuously incorporates suggestions from developers who build applications on BlinkGTK. Please send proposals for
new patterns as GitHub issues.

Planned



Suggested by: developers who build applications on BlinkGTK
Compiled and published by: BlinkGTK Project