LibWeb: Stop blocking main fetch on pending preload results

The lichess.org lobby stylesheet sometimes gets render-blocked,
resulting in a blank page.

The main fetch would queue work and then spin_until wait for a pending
preload result on the main thread. This re-entered the event loop and
stranded the queued PendingResponse callback behind a pile of nested
event processing. So the preload response existed, but the stylesheet
never made it through the normal handoff path.

I chose to follow the PreloadEntry.cpp model to address this and track
at most one pending preload PendingResponse for each FetchParams,
resolving it directly from the preload callback when the response
arrives. This removes the spin_until, keeps the handoff asynchronous,
and lets the blocked stylesheet finish loading normally.

If the current single-consumer preload path ever changes, this logic
must be widened to handle it.
This commit is contained in:
Jonathan Gamble 2026-05-04 23:33:53 -05:00 committed by Shannon Booth
parent 4668e7ed11
commit 6686cecf72
3 changed files with 37 additions and 4 deletions

View file

@ -229,6 +229,14 @@ GC::Ref<Infrastructure::FetchController> fetch(JS::Realm& realm, Infrastructure:
// response: set fetchParamss preloaded response candidate to response.
auto on_preloaded_response_available = GC::create_function(realm.heap(), [fetch_params](GC::Ref<Infrastructure::Response> response) {
fetch_params->set_preloaded_response_candidate(response);
// NB: main fetch may already be parked on this preload result.
// Resolve that waiter here instead of spinning the event loop.
auto controller = fetch_params->controller();
if (auto pending_preloaded_response = controller->pending_preloaded_response()) {
controller->set_pending_preloaded_response(nullptr);
pending_preloaded_response->resolve(response);
}
});
// 3. Let foundPreloadedResource be the result of invoking consume a preloaded resource for requests
@ -456,10 +464,22 @@ GC::Ptr<PendingResponse> main_fetch(JS::Realm& realm, Infrastructure::FetchParam
// -> fetchParamss preloaded response candidate is not null
if (!fetch_params.preloaded_response_candidate().has<Empty>()) {
// 1. Wait until fetchParamss preloaded response candidate is not "pending".
HTML::main_thread_event_loop().spin_until(GC::create_function(vm.heap(), [&] {
return !fetch_params.preloaded_response_candidate().has<Infrastructure::FetchParams::PreloadedResponseCandidatePendingTag>();
}));
// 1. Wait until fetchParams's preloaded response candidate is not "pending".
if (fetch_params.preloaded_response_candidate().has<Infrastructure::FetchParams::PreloadedResponseCandidatePendingTag>()) {
// NB: The spec says to wait here. Spinning the main-thread event
// loop strands the queued preload callback behind nested work,
// so we park a PendingResponse and let that callback resolve it.
auto controller = fetch_params.controller();
// NB: Reuse the parked response if this branch is re-entered
// before the preload callback runs.
if (auto pending_preloaded_response = controller->pending_preloaded_response())
return *pending_preloaded_response;
auto pending_preloaded_response = PendingResponse::create(vm, request);
controller->set_pending_preloaded_response(pending_preloaded_response);
return pending_preloaded_response;
}
// 2. Assert: fetchParamss preloaded response candidate is a response.
VERIFY(fetch_params.preloaded_response_candidate().has<GC::Ref<Infrastructure::Response>>());

View file

@ -7,6 +7,7 @@
#include <LibGC/Heap.h>
#include <LibJS/Runtime/VM.h>
#include <LibRequests/Request.h>
#include <LibWeb/Fetch/Fetching/PendingResponse.h>
#include <LibWeb/Fetch/Infrastructure/FetchAlgorithms.h>
#include <LibWeb/Fetch/Infrastructure/FetchController.h>
#include <LibWeb/Fetch/Infrastructure/FetchParams.h>
@ -32,6 +33,7 @@ void FetchController::visit_edges(JS::Cell::Visitor& visitor)
visitor.visit(m_report_timing_steps);
visitor.visit(m_next_manual_redirect_steps);
visitor.visit(m_fetch_params);
visitor.visit(m_pending_preloaded_response);
}
void FetchController::set_pending_request(RefPtr<Requests::Request> const& request)

View file

@ -21,6 +21,12 @@
#include <LibWeb/HTML/EventLoop/Task.h>
#include <LibWeb/HTML/StructuredSerializeTypes.h>
namespace Web::Fetch::Fetching {
class PendingResponse;
}
namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#fetch-controller
@ -52,6 +58,8 @@ public:
void terminate();
void set_fetch_params(Badge<FetchParams>, GC::Ref<FetchParams> fetch_params) { m_fetch_params = fetch_params; }
[[nodiscard]] GC::Ptr<Fetching::PendingResponse> pending_preloaded_response() const { return m_pending_preloaded_response; }
void set_pending_preloaded_response(GC::Ptr<Fetching::PendingResponse> pending_preloaded_response) { m_pending_preloaded_response = pending_preloaded_response; }
void set_pending_request(RefPtr<Requests::Request> const&);
void set_inner_fetch_controller(GC::Ref<FetchController>);
@ -94,6 +102,9 @@ private:
GC::Ptr<GC::Function<void()>> m_next_manual_redirect_steps;
GC::Ptr<FetchParams> m_fetch_params;
// NB: Assumes one waiting consumer for a pending preloaded response.
// Widen this if preload handoff ever supports multiple consumers.
GC::Ptr<Fetching::PendingResponse> m_pending_preloaded_response;
WeakPtr<Requests::Request> m_pending_request;