LibWeb: Block rendering until linked stylesheets are loaded

This commit implements the main "render blocking" behavior for link
elements, drastically reducing the amount of FOUC (flash of unstyled
content) we subject our users to.

The document will now block rendering until linked style sheets
referenced by parser-created link elements have loaded (or failed).

Note that we don't yet extend the blocking period until "critical
subresources" such as imported style sheets have been downloaded
as well.
This commit is contained in:
Andreas Kling 2025-02-27 15:30:26 +01:00 committed by Andreas Kling
parent 5146bbe296
commit 043e96946f
Notes: github-actions[bot] 2025-02-27 20:37:32 +00:00
5 changed files with 52 additions and 8 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2024, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2021-2025, Luke Wilde <luke@ladybird.org>
* Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
@ -6043,12 +6043,26 @@ bool Document::allow_declarative_shadow_roots() const
return m_allow_declarative_shadow_roots;
}
bool Document::is_render_blocking_element(GC::Ref<Element> element) const
{
return m_render_blocking_elements.contains(element);
}
// https://html.spec.whatwg.org/multipage/dom.html#render-blocked
bool Document::is_render_blocked() const
{
// A Document document is render-blocked if both of the following are true:
// - document's render-blocking element set is non-empty, or document allows adding render-blocking elements.
// - FIXME: The current high resolution time given document's relevant global object has not exceeded an implementation-defined timeout value.
// - The current high resolution time given document's relevant global object has not exceeded an implementation-defined timeout value.
// NOTE: This timeout is implementation-defined.
// Other browsers are willing to wait longer, but let's start with 30 seconds.
static constexpr auto max_time_to_block_rendering_in_ms = 30000.0;
auto now = HighResolutionTime::current_high_resolution_time(relevant_global_object(*this));
if (now > max_time_to_block_rendering_in_ms)
return false;
return !m_render_blocking_elements.is_empty() || allows_adding_render_blocking_elements();
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2023-2024, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
@ -813,6 +813,8 @@ public:
// https://html.spec.whatwg.org/multipage/dom.html#allows-adding-render-blocking-elements
[[nodiscard]] bool allows_adding_render_blocking_elements() const;
[[nodiscard]] bool is_render_blocking_element(GC::Ref<Element>) const;
void add_render_blocking_element(GC::Ref<Element>);
void remove_render_blocking_element(GC::Ref<Element>);

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, the SerenityOS developers.
* Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2023, Srikavin Ramkumar <me@srikavin.me>
@ -497,6 +497,8 @@ void HTMLLinkElement::process_stylesheet_resource(bool success, Fetch::Infrastru
// FIXME: 2. Decrement el's node document's script-blocking style sheet counter by 1.
// 7. Unblock rendering on el.
unblock_rendering();
m_document_load_event_delayer.clear();
}
@ -526,16 +528,30 @@ bool HTMLLinkElement::stylesheet_linked_resource_fetch_setup_steps(Fetch::Infras
// 3. If el's media attribute's value matches the environment and el is potentially render-blocking, then block rendering on el.
// FIXME: Check media attribute value.
if (is_potentially_render_blocking())
block_rendering();
m_document_load_event_delayer.emplace(document());
// 4. If el is currently render-blocking, then set request's render-blocking to true.
// FIXME: Check if el is currently render-blocking.
request.set_render_blocking(true);
if (document().is_render_blocking_element(*this))
request.set_render_blocking(true);
// 5. Return true.
return true;
}
void HTMLLinkElement::set_parser_document(Badge<HTMLParser>, GC::Ref<DOM::Document> document)
{
m_parser_document = document->make_weak_ptr<DOM::Document>();
}
bool HTMLLinkElement::is_implicitly_potentially_render_blocking() const
{
// A link element of this type is implicitly potentially render-blocking if the element was created by its node document's parser.
return &document() == m_parser_document;
}
void HTMLLinkElement::resource_did_load_favicon()
{
VERIFY(m_relationship & (Relationship::Icon));

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, the SerenityOS developers.
* Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2023, Srikavin Ramkumar <me@srikavin.me>
@ -43,6 +43,8 @@ public:
static WebIDL::ExceptionOr<void> load_fallback_favicon_if_needed(GC::Ref<DOM::Document>);
void set_parser_document(Badge<HTMLParser>, GC::Ref<DOM::Document>);
private:
HTMLLinkElement(DOM::Document&, DOM::QualifiedName);
@ -58,6 +60,7 @@ private:
// ^HTMLElement
virtual void visit_edges(Cell::Visitor&) override;
virtual bool is_implicitly_potentially_render_blocking() const override;
struct LinkProcessingOptions {
// href (default the empty string)
@ -152,6 +155,8 @@ private:
bool m_explicitly_enabled { false };
Optional<String> m_mime_type;
WeakPtr<DOM::Document> m_parser_document;
};
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020-2024, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2020-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2023-2024, Shannon Booth <shannon@serenityos.org>
*
@ -30,6 +30,7 @@
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/HTML/HTMLHeadElement.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/HTMLLinkElement.h>
#include <LibWeb/HTML/HTMLScriptElement.h>
#include <LibWeb/HTML/HTMLTableElement.h>
#include <LibWeb/HTML/HTMLTemplateElement.h>
@ -781,6 +782,12 @@ GC::Ref<DOM::Element> HTMLParser::create_element_for(HTMLToken const& token, Opt
// 9. Let element be the result of creating an element given document, localName, given namespace, null, is, and willExecuteScript.
auto element = create_element(*document, local_name, namespace_, {}, is_value, will_execute_script).release_value_but_fixme_should_propagate_errors();
// AD-HOC: Let <link> elements know which document they were originally parsed for.
// This is used for the render-blocking logic.
if (local_name == HTML::TagNames::link && namespace_ == Namespace::HTML) {
as<HTMLLinkElement>(*element).set_parser_document({}, document);
}
// 10. Append each attribute in the given token to element.
token.for_each_attribute([&](auto const& attribute) {
DOM::QualifiedName qualified_name { attribute.local_name, attribute.prefix, attribute.namespace_ };