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 client implementers (Issue #81 and others).


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 (Issue #78-class assertions) 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 client implementers. Please send proposals for
new patterns as GitHub issues.

Planned



Reported originally by: the client implementer community (Issue #81)
Compiled and published by: BlinkGTK Project