offline: make startup and browsing work without a connection (#854) - #1694
Open
Suncadgeek wants to merge 8 commits into
Open
offline: make startup and browsing work without a connection (#854)#1694Suncadgeek wants to merge 8 commits into
Suncadgeek wants to merge 8 commits into
Conversation
Add a network timeout to the NetworkFirst route for HTML pages, so that the app shell is served from cache when the network is slow or dead instead of blocking the startup indefinitely. Register the service worker immediately when running inside the native app: sessions there can be short-lived, and the service worker must take control early for offline startup to work.
The first render of the timeline waited on the network in several places, so starting the app offline (or on a connection that drops packets, e.g. behind a reconnecting VPN) showed a blank page for up to a minute, or forever on a first start: - The on-this-day widget blocked the first timeline render while revalidating over the network. When cached content is available, revalidate in the background instead. - The static configuration request had no timeout and the whole app waits on it. Bound it to 5 seconds and retry in the background with backoff on failure; a new bus event tells UserConfig components to pick up late-loaded values. Without this, a first start with a failed request rendered nothing at all (timeline_path _unknown_); also show a loading icon instead of a blank page in that state. - Treat client-side timeouts (ECONNABORTED) as network errors so they do not surface as error toasts. - Tell the user once when cached content is shown offline, and refresh the timeline when connectivity returns (online event).
The image worker opened the Cache API storage asynchronously at startup, but the first batch of preview requests arrives before the open completes. Those requests silently skipped the cache lookup (imageCache is still undefined) and went straight to the network queue. Online this is invisible; offline the whole first viewport stays on placeholders forever even though every preview is cached. Keep the opening promise and await it before matching.
On Android WebView, worker script requests are not routed through the service worker, so the image worker cannot start at all while offline: the fetch of its own script hangs, and every preview request queues against a worker that will never answer. No error surfaces anywhere. Race the worker fetch against a timeout and serve the image from the main thread (cache first) when it expires. Once the worker is marked unresponsive further requests skip the wait; a later successful worker response re-enables it.
After the timeline loads over the network in the native app, warm the offline caches in the background with the most recent days (up to 500 photos): the day details go to the data cache and the previews to the image cache, through the existing cache-first worker path so anything already cached costs nothing. Throttled to once per hour.
Two startup paths waited on network timeouts before rendering content that was available locally: - The data cache name awaited the remote configuration just to read the version. Race it against the persisted version (1s) so cached days render without waiting for the config request to fail. - The image worker fallback waited 5s on the first image request. Replace the per-request timeout with a single ping health check (2s deadline): if the worker does not answer, all requests go to the main thread immediately; if it does, requests are never raced against a timer, so slow networks cannot cause double fetches. Re-probe every 30s so the worker is used again once it can start.
Startup no longer waits on the network round trip for the HTML page: serve the cached copy immediately and revalidate in the background. This makes the app start in constant time regardless of connection quality, which matters most on slow mobile networks. The embedded initial state and request token can be one navigation stale. The timeline refreshes itself over the API on every load, and the request token remains valid for the session lifetime; renewal goes through the network-only /csrftoken route.
The precache stored entries keyed by the webpack output name, which carries the content hash as a ?v= parameter. Pages request the same scripts with a different ?v= (the Nextcloud version), so no page request ever matched the precache: scripts were served forever by the runtime cache-first route, and deploying a new version had no effect until that cache expired. Move the content hash into the workbox revision so the cache key is the bare URL, ignore the ?v= parameter when matching, and activate updated workers immediately (skipWaiting): revisioned entries keep an already-open page consistent.
Suncadgeek
force-pushed
the
pr-offline-web
branch
from
July 3, 2026 22:14
362865c to
0d7b3aa
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The service worker and the data and image caches already put most of the offline pieces in place, but several startup paths still block on the network, so in practice launching the app offline (or on a flaky connection) shows a blank page. This series fixes them one by one; each commit is independently reviewable. It also addresses the report in #854 that the whole app waits to connect before showing anything (#854 (comment)).
Bug fixes:
The image worker opened its cache asynchronously at startup, and the first viewport of preview requests raced past the check while it was still undefined, going straight to the network. Offline, the whole first screen stayed on placeholders forever even though every preview was cached.
On Android WebView, worker script requests are not routed through the service worker at all, so the image worker cannot even start while offline. The worker is now health-checked once with a short deadline and requests fall back to the main thread (cache first) when it does not answer.
The precache stored entries keyed by the webpack output name, which carries the content hash as a ?v= parameter, while pages request the same scripts with the Nextcloud version as ?v=. No page request ever matched the precache, so deployed updates never reached browsers until the runtime cache expired. Revisions are moved out of the URL, the ?v= parameter is ignored when matching, and updated workers activate immediately.
The static configuration request had no timeout and much of the app waits on it. A first start with a failed request rendered nothing at all, permanently. It is now bounded, retried in the background with backoff, and the app shows a loading indicator instead of a blank page in that state.
The on-this-day widget blocked the first timeline render while revalidating over the network. When cached content is available it now revalidates in the background.
Behavior improvements:
HTML pages are served stale-while-revalidate, so startup does not depend on the network round trip and takes constant time regardless of connection quality.
The most recent days (up to 500 photos) are prefetched in the background after the timeline loads in the native app, so recently synced content can be browsed offline even if it was never displayed. Throttled to once per hour and served through the existing cache-first worker path, so anything already cached costs nothing.
The user is told once when cached content is being shown offline, and the timeline refreshes when connectivity returns.
Client-side timeouts are treated as network errors so they do not surface as error toasts.
Test environment: Nextcloud 34.0.0 (PHP 8.4, Docker) with Memories 8.0.1, self-hosted on fiber. Measured on a Pixel 7 (GrapheneOS, Vanadium WebView): offline cold start went from a permanently blank page to skeleton in about 4 seconds and cached photos in about 7, and browsing weeks of never-displayed days offline works. Online startup is unchanged or slightly faster. Also verified on a Samsung Galaxy S10 (stock Android and WebView) running the unmodified store build of the app against the same server, with identical results.