From 043e96946f76cc7b154649d55c43e91a45503f03 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 27 Feb 2025 15:30:26 +0100 Subject: [PATCH] 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. --- Libraries/LibWeb/DOM/Document.cpp | 18 +++++++++++++++-- Libraries/LibWeb/DOM/Document.h | 4 +++- Libraries/LibWeb/HTML/HTMLLinkElement.cpp | 22 ++++++++++++++++++--- Libraries/LibWeb/HTML/HTMLLinkElement.h | 7 ++++++- Libraries/LibWeb/HTML/Parser/HTMLParser.cpp | 9 ++++++++- 5 files changed, 52 insertions(+), 8 deletions(-) diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 829948a752d..da40421c493 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2024, Andreas Kling + * Copyright (c) 2018-2025, Andreas Kling * Copyright (c) 2021-2023, Linus Groh * Copyright (c) 2021-2025, Luke Wilde * Copyright (c) 2021-2024, Sam Atkins @@ -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) 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(); } diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index cf636f8f223..2badbcc273c 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2023, Andreas Kling + * Copyright (c) 2018-2025, Andreas Kling * Copyright (c) 2021-2023, Linus Groh * Copyright (c) 2023-2024, Shannon Booth * Copyright (c) 2025, Jelle Raaijmakers @@ -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) const; + void add_render_blocking_element(GC::Ref); void remove_render_blocking_element(GC::Ref); diff --git a/Libraries/LibWeb/HTML/HTMLLinkElement.cpp b/Libraries/LibWeb/HTML/HTMLLinkElement.cpp index 164310b6916..0699d16b64e 100644 --- a/Libraries/LibWeb/HTML/HTMLLinkElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLLinkElement.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2023, Andreas Kling + * Copyright (c) 2018-2025, Andreas Kling * Copyright (c) 2021, the SerenityOS developers. * Copyright (c) 2021, Sam Atkins * Copyright (c) 2023, Srikavin Ramkumar @@ -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, GC::Ref document) +{ + m_parser_document = document->make_weak_ptr(); +} + +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)); diff --git a/Libraries/LibWeb/HTML/HTMLLinkElement.h b/Libraries/LibWeb/HTML/HTMLLinkElement.h index 3b909ac8d19..675e712585c 100644 --- a/Libraries/LibWeb/HTML/HTMLLinkElement.h +++ b/Libraries/LibWeb/HTML/HTMLLinkElement.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2023, Andreas Kling + * Copyright (c) 2018-2025, Andreas Kling * Copyright (c) 2021, the SerenityOS developers. * Copyright (c) 2021, Sam Atkins * Copyright (c) 2023, Srikavin Ramkumar @@ -43,6 +43,8 @@ public: static WebIDL::ExceptionOr load_fallback_favicon_if_needed(GC::Ref); + void set_parser_document(Badge, GC::Ref); + 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 m_mime_type; + + WeakPtr m_parser_document; }; } diff --git a/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp b/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp index c68bb4063a1..a91be2c4357 100644 --- a/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp +++ b/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024, Andreas Kling + * Copyright (c) 2020-2025, Andreas Kling * Copyright (c) 2021, Luke Wilde * Copyright (c) 2023-2024, Shannon Booth * @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -781,6 +782,12 @@ GC::Ref 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 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(*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_ };