Fixed HTML parser performance for large inline scripts/styles [chromium/src : main]

0 views
Skip to first unread message

Patrick Meenan (Gerrit)

unread,
Apr 8, 2026, 1:15:57 PMApr 8
to Mason Freed, AyeAye, Chromium LUCI CQ, chromium...@chromium.org, jmedle...@chromium.org, blink-rev...@chromium.org, blink-...@chromium.org, kinuko...@chromium.org, loading-rev...@chromium.org
Attention needed from Mason Freed

Patrick Meenan added 1 comment

Patchset-level comments
File-level comment, Patchset 7 (Latest):
Patrick Meenan . resolved

Mason, could you PTAL when you get a chance (1-line fix + a few tests).

This fixes the edge case in parser performance, particularly on low-end android devices that I mentioned a while back (turns out it was the tree builder, not the raw tokenizing).

I have it behind a killswitch so we can also do a finch holdback to measure impact.

Open in Gerrit

Related details

Attention is currently required from:
  • Mason Freed
Submit Requirements:
  • requirement satisfiedCode-Coverage
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: chromium/src
Gerrit-Branch: main
Gerrit-Change-Id: I3ad0551aae691de9be6bb66230a8a88dcd38917d
Gerrit-Change-Number: 7737055
Gerrit-PatchSet: 7
Gerrit-Owner: Patrick Meenan <pme...@chromium.org>
Gerrit-Reviewer: Mason Freed <mas...@chromium.org>
Gerrit-Reviewer: Patrick Meenan <pme...@chromium.org>
Gerrit-Attention: Mason Freed <mas...@chromium.org>
Gerrit-Comment-Date: Wed, 08 Apr 2026 17:15:52 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
satisfied_requirement
unsatisfied_requirement
open
diffy

Mason Freed (Gerrit)

unread,
Apr 13, 2026, 6:52:53 PMApr 13
to Patrick Meenan, android-bu...@system.gserviceaccount.com, chromiu...@luci-project-accounts.iam.gserviceaccount.com, chromium...@chromium.org, jmedle...@chromium.org, blink-rev...@chromium.org, blink-...@chromium.org, kinuko...@chromium.org, loading-rev...@chromium.org
Attention needed from Patrick Meenan

Mason Freed added 8 comments

Patchset-level comments
File-level comment, Patchset 13 (Latest):
Mason Freed . resolved

Overall, interesting patch/approach. I think I like it. Just some comments about testing and the constants.

File third_party/blink/renderer/core/html/parser/html_document_parser_test.cc
Line 10, Patchset 13 (Latest):#include "base/run_loop.h"
Mason Freed . unresolved

is this used?

Line 169, Patchset 13 (Latest):TEST_P(HTMLDocumentParserTest, DeferTreeBuilderFlushEOF) {
Mason Freed . unresolved

So these tests are a race against the initial 125ms delay, which could cause them to be flaky. What about using a mock time source, so you can avoid that, plus you can also fast-forward the clock and make sure things get flushed after the backoff time elapses?

Line 220, Patchset 13 (Latest): // With the flush deferreing feature disabled, flushes in kTextMode should
Mason Freed . unresolved

nit: deferring

Line 231, Patchset 13 (Latest): EXPECT_EQ("aaa", script->firstChild()->nodeValue());
}
Mason Freed . unresolved

This test doesn't try flushing two chunks, so it'll pass either way, right?

File third_party/blink/renderer/core/html/parser/html_tree_builder.h
Line 268, Patchset 13 (Latest): // Used to implement incremental backoff for flushes.
Mason Freed . unresolved

Perhaps add what `nullopt` means also?

File third_party/blink/renderer/core/html/parser/html_tree_builder.cc
Line 79, Patchset 13 (Latest): base::Milliseconds(125);
Mason Freed . unresolved
historically, magic time constants like this in the parser:
a) tend to be very material, in terms of eventual performance,
b) set by Finch at first, in order to experiment

Absent that, this at least needs more of a comment about how you arrived at `125`.
Line 81, Patchset 13 (Latest):// rendering too long.
Mason Freed . unresolved

Similar comment here. E.g. perhaps 2.5 seconds, to match the "good" LCP threshold?

Open in Gerrit

Related details

Attention is currently required from:
  • Patrick Meenan
Submit Requirements:
    • requirement satisfiedCode-Coverage
    • requirement is not satisfiedCode-Owners
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: chromium/src
    Gerrit-Branch: main
    Gerrit-Change-Id: I3ad0551aae691de9be6bb66230a8a88dcd38917d
    Gerrit-Change-Number: 7737055
    Gerrit-PatchSet: 13
    Gerrit-Owner: Patrick Meenan <pme...@chromium.org>
    Gerrit-Reviewer: Mason Freed <mas...@chromium.org>
    Gerrit-Reviewer: Patrick Meenan <pme...@chromium.org>
    Gerrit-Attention: Patrick Meenan <pme...@chromium.org>
    Gerrit-Comment-Date: Mon, 13 Apr 2026 22:52:41 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    satisfied_requirement
    unsatisfied_requirement
    open
    diffy

    Patrick Meenan (Gerrit)

    unread,
    Apr 14, 2026, 10:37:00 PMApr 14
    to Mason Freed, android-bu...@system.gserviceaccount.com, chromiu...@luci-project-accounts.iam.gserviceaccount.com, chromium...@chromium.org, blink-re...@chromium.org, jmedle...@chromium.org, blink-rev...@chromium.org, blink-...@chromium.org, kinuko...@chromium.org, loading-rev...@chromium.org
    Attention needed from Mason Freed

    Patrick Meenan added 8 comments

    Patchset-level comments
    File-level comment, Patchset 16 (Latest):
    Patrick Meenan . resolved

    Thanks. I appreciate you weighing in on this. I cleaned up the tests, moved the params to finch-configurable and linked to the crbug with the details.

    My main question is default-enabled or default-disabled? I'm leaning pretty hard towards default-enabled becauce if it is going to break some edge case it is a LOT easier to detect and bisect if it isn't rolled out by finch and we can run a holdback to measure the impact.

    I could easily be convinced to go the other wat though and ship disabled, verify no regressions and then flip to enabled.

    File third_party/blink/renderer/core/html/parser/html_document_parser_test.cc
    Line 10, Patchset 13:#include "base/run_loop.h"
    Mason Freed . resolved

    is this used?

    Patrick Meenan

    Sorry, was from an earlier round of tests. Removed.

    Line 169, Patchset 13:TEST_P(HTMLDocumentParserTest, DeferTreeBuilderFlushEOF) {
    Mason Freed . resolved

    So these tests are a race against the initial 125ms delay, which could cause them to be flaky. What about using a mock time source, so you can avoid that, plus you can also fast-forward the clock and make sure things get flushed after the backoff time elapses?

    Patrick Meenan

    Done

    Line 220, Patchset 13: // With the flush deferreing feature disabled, flushes in kTextMode should
    Mason Freed . resolved

    nit: deferring

    Patrick Meenan

    Done

    Line 231, Patchset 13: EXPECT_EQ("aaa", script->firstChild()->nodeValue());
    }
    Mason Freed . resolved

    This test doesn't try flushing two chunks, so it'll pass either way, right?

    Patrick Meenan

    Fixed, thanks.

    File third_party/blink/renderer/core/html/parser/html_tree_builder.h
    Line 268, Patchset 13: // Used to implement incremental backoff for flushes.
    Mason Freed . resolved

    Perhaps add what `nullopt` means also?

    Patrick Meenan

    Done

    File third_party/blink/renderer/core/html/parser/html_tree_builder.cc
    Line 79, Patchset 13: base::Milliseconds(125);
    Mason Freed . resolved
    historically, magic time constants like this in the parser:
    a) tend to be very material, in terms of eventual performance,
    b) set by Finch at first, in order to experiment

    Absent that, this at least needs more of a comment about how you arrived at `125`.
    Patrick Meenan

    I moved the magic numbers into finch params and referenced the crbug with the background on how the numbers were derived.

    Line 81, Patchset 13:// rendering too long.
    Mason Freed . resolved

    Similar comment here. E.g. perhaps 2.5 seconds, to match the "good" LCP threshold?

    Patrick Meenan

    Acknowledged

    Open in Gerrit

    Related details

    Attention is currently required from:
    • Mason Freed
    Submit Requirements:
      • requirement satisfiedCode-Coverage
      • requirement is not satisfiedCode-Owners
      • requirement is not satisfiedCode-Review
      • requirement is not satisfiedReview-Enforcement
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: chromium/src
      Gerrit-Branch: main
      Gerrit-Change-Id: I3ad0551aae691de9be6bb66230a8a88dcd38917d
      Gerrit-Change-Number: 7737055
      Gerrit-PatchSet: 16
      Gerrit-Attention: Mason Freed <mas...@chromium.org>
      Gerrit-Comment-Date: Wed, 15 Apr 2026 02:36:52 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: No
      Comment-In-Reply-To: Mason Freed <mas...@chromium.org>
      satisfied_requirement
      unsatisfied_requirement
      open
      diffy

      Mason Freed (Gerrit)

      unread,
      Apr 15, 2026, 6:54:26 PMApr 15
      to Patrick Meenan, android-bu...@system.gserviceaccount.com, Chromium LUCI CQ, chromium...@chromium.org, blink-re...@chromium.org, jmedle...@chromium.org, blink-rev...@chromium.org, blink-...@chromium.org, kinuko...@chromium.org, loading-rev...@chromium.org
      Attention needed from Patrick Meenan

      Mason Freed voted and added 7 comments

      Votes added by Mason Freed

      Code-Review+1

      7 comments

      Patchset-level comments
      Patrick Meenan . resolved

      Thanks. I appreciate you weighing in on this. I cleaned up the tests, moved the params to finch-configurable and linked to the crbug with the details.

      My main question is default-enabled or default-disabled? I'm leaning pretty hard towards default-enabled becauce if it is going to break some edge case it is a LOT easier to detect and bisect if it isn't rolled out by finch and we can run a holdback to measure the impact.

      I could easily be convinced to go the other wat though and ship disabled, verify no regressions and then flip to enabled.

      Mason Freed

      Thanks also for writing up the descriptions and local testing results, e.g. here:

      https://crbug.com/500385603#comment3

      That makes me feel a TON better about the choices made here for the backoff values.

      Mason Freed . resolved

      This looks pretty good to me! Just some small nits and things, but LGTM. Good luck! Let me know how the results look.

      File third_party/blink/common/features.cc
      Line 493, Patchset 16 (Latest):BASE_FEATURE_PARAM(base::TimeDelta,
      kDeferTreeBuilderFlushInitialInterval,
      &kDeferTreeBuilderFlush,
      "initial_interval",
      base::Milliseconds(16));

      BASE_FEATURE_PARAM(base::TimeDelta,
      kDeferTreeBuilderFlushMaxInterval,
      &kDeferTreeBuilderFlush,
      "max_interval",
      base::Seconds(2));
      Mason Freed . resolved

      Based on your local testing, these feel great to me, at least as defaults.

      Line 506, Patchset 16 (Latest): kDeferTreeBuilderFlushMultiplier,
      Mason Freed . unresolved

      This one might be overkill. Obviously it's a tuning parameter, but `2.0` feels like a natural number that doesn't need experimental verification. Of course, keep it if you want it!

      File third_party/blink/renderer/core/html/parser/html_document_parser_test.cc
      Line 138, Patchset 16 (Latest): task_environment().FastForwardBy(base::Milliseconds(0));
      Mason Freed . unresolved

      Does this do anything? fast forward by zero?

      Line 174, Patchset 16 (Latest): task_environment().FastForwardBy(base::Milliseconds(0));
      Mason Freed . unresolved

      In particular, I'm hoping nothing needs to happen here, so that the Flush above results in the `ddd` getting added below.

      File third_party/blink/renderer/core/html/parser/html_tree_builder.cc
      Line 3135, Patchset 16 (Latest): if (base::FeatureList::IsEnabled(features::kDeferTreeBuilderFlush) &&
      Mason Freed . unresolved

      Have they made FeatureList::IsEnabled fast now? I recall there being performance penalties in the past, such that people would cache the bit in a local static var. If you know that's not needed, fine. Just checking, to avoid a regression due to this check.

      If nothing else, reversing the order of the two conditions (putting `insertion_mode_ ==` first) might help anyway.

      Open in Gerrit

      Related details

      Attention is currently required from:
      • Patrick Meenan
      Submit Requirements:
      • requirement satisfiedCode-Coverage
      • requirement satisfiedCode-Owners
      • requirement satisfiedCode-Review
      • requirement is not satisfiedNo-Unresolved-Comments
      • requirement satisfiedReview-Enforcement
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: chromium/src
      Gerrit-Branch: main
      Gerrit-Change-Id: I3ad0551aae691de9be6bb66230a8a88dcd38917d
      Gerrit-Change-Number: 7737055
      Gerrit-PatchSet: 16
      Gerrit-Owner: Patrick Meenan <pme...@chromium.org>
      Gerrit-Reviewer: Mason Freed <mas...@chromium.org>
      Gerrit-Reviewer: Patrick Meenan <pme...@chromium.org>
      Gerrit-Attention: Patrick Meenan <pme...@chromium.org>
      Gerrit-Comment-Date: Wed, 15 Apr 2026 22:54:14 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: Yes
      Comment-In-Reply-To: Patrick Meenan <pme...@chromium.org>
      satisfied_requirement
      unsatisfied_requirement
      open
      diffy

      Patrick Meenan (Gerrit)

      unread,
      Apr 15, 2026, 7:59:30 PMApr 15
      to Mason Freed, android-bu...@system.gserviceaccount.com, Chromium LUCI CQ, chromium...@chromium.org, blink-re...@chromium.org, jmedle...@chromium.org, blink-rev...@chromium.org, blink-...@chromium.org, kinuko...@chromium.org, loading-rev...@chromium.org

      Patrick Meenan added 5 comments

      Patchset-level comments
      File-level comment, Patchset 17 (Latest):
      Patrick Meenan . resolved

      Thanks. I added caching to the feature flag just to be safe. Let me know if the 0ms fast forwarding is a concern and I can track down where the PostTask is happening that is interfering with the test (or find another way to trigger it without RunUntilIdle).

      File third_party/blink/common/features.cc
      Line 506, Patchset 16: kDeferTreeBuilderFlushMultiplier,
      Mason Freed . resolved

      This one might be overkill. Obviously it's a tuning parameter, but `2.0` feels like a natural number that doesn't need experimental verification. Of course, keep it if you want it!

      Patrick Meenan

      It's mostly here in case we wanted to try a steeper or shallower exponential backoff, to favor more content earlier or to accumulate more quicker without changing the initial delay. My guess is we won't change it but the 4.0 multiplier with a 16ms initial performed better than a 2.0 multiplier with a 30ms initial so it's probably valuable to keep for now.

      File third_party/blink/renderer/core/html/parser/html_document_parser_test.cc
      Line 138, Patchset 16: task_environment().FastForwardBy(base::Milliseconds(0));
      Mason Freed . resolved

      Does this do anything? fast forward by zero?

      Patrick Meenan

      It's basically a way to run the tasks that were queued by the parser flush to deterministically make sure the script element was created on the document but without advancing the clock (now that RunUntilIdle is precommit-warned against using). RunUntil() with a closure has the nasty side-effect of automatically advancing the mock clock if it needs to until the condition is met (which triggers our time-based delay).

      Line 174, Patchset 16: task_environment().FastForwardBy(base::Milliseconds(0));
      Mason Freed . resolved

      In particular, I'm hoping nothing needs to happen here, so that the Flush above results in the `ddd` getting added below.

      Patrick Meenan

      Nothing that we added, but I think somewhere inside of the tree builder something is queued on a posted task when incrementally appending to the existing node. Let me know if there is a concern that something unintentional is getting triggered and if it should all flow directly from Flush() without task processing.

      File third_party/blink/renderer/core/html/parser/html_tree_builder.cc
      Line 3135, Patchset 16: if (base::FeatureList::IsEnabled(features::kDeferTreeBuilderFlush) &&
      Mason Freed . resolved

      Have they made FeatureList::IsEnabled fast now? I recall there being performance penalties in the past, such that people would cache the bit in a local static var. If you know that's not needed, fine. Just checking, to avoid a regression due to this check.

      If nothing else, reversing the order of the two conditions (putting `insertion_mode_ ==` first) might help anyway.

      Patrick Meenan

      It is supposed to be cached as of a few years ago but that also made me a bit nervous so I'll went ahead and switched it to use the same static pattern used elsewhere in the parser just to be safe.

      Open in Gerrit

      Related details

      Attention set is empty
      Submit Requirements:
        • requirement satisfiedCode-Coverage
        • requirement satisfiedCode-Owners
        • requirement satisfiedCode-Review
        • requirement satisfiedReview-Enforcement
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: comment
        Gerrit-Project: chromium/src
        Gerrit-Branch: main
        Gerrit-Change-Id: I3ad0551aae691de9be6bb66230a8a88dcd38917d
        Gerrit-Change-Number: 7737055
        Gerrit-PatchSet: 17
        Gerrit-Owner: Patrick Meenan <pme...@chromium.org>
        Gerrit-Reviewer: Mason Freed <mas...@chromium.org>
        Gerrit-Reviewer: Patrick Meenan <pme...@chromium.org>
        Gerrit-Comment-Date: Wed, 15 Apr 2026 23:59:23 +0000
        satisfied_requirement
        open
        diffy

        Patrick Meenan (Gerrit)

        unread,
        Apr 17, 2026, 4:08:32 PM (13 days ago) Apr 17
        to Mason Freed, android-bu...@system.gserviceaccount.com, Chromium LUCI CQ, chromium...@chromium.org, blink-re...@chromium.org, jmedle...@chromium.org, blink-rev...@chromium.org, blink-...@chromium.org, kinuko...@chromium.org, loading-rev...@chromium.org

        Patrick Meenan voted Commit-Queue+2

        Commit-Queue+2
        Open in Gerrit

        Related details

        Attention set is empty
        Submit Requirements:
        • requirement satisfiedCode-Coverage
        • requirement satisfiedCode-Owners
        • requirement satisfiedCode-Review
        • requirement satisfiedReview-Enforcement
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: comment
        Gerrit-Project: chromium/src
        Gerrit-Branch: main
        Gerrit-Change-Id: I3ad0551aae691de9be6bb66230a8a88dcd38917d
        Gerrit-Change-Number: 7737055
        Gerrit-PatchSet: 18
        Gerrit-Owner: Patrick Meenan <pme...@chromium.org>
        Gerrit-Reviewer: Mason Freed <mas...@chromium.org>
        Gerrit-Reviewer: Patrick Meenan <pme...@chromium.org>
        Gerrit-Comment-Date: Fri, 17 Apr 2026 20:08:24 +0000
        Gerrit-HasComments: No
        Gerrit-Has-Labels: Yes
        satisfied_requirement
        open
        diffy

        Chromium LUCI CQ (Gerrit)

        unread,
        Apr 17, 2026, 5:44:01 PM (12 days ago) Apr 17
        to Patrick Meenan, Mason Freed, android-bu...@system.gserviceaccount.com, chromium...@chromium.org, blink-re...@chromium.org, jmedle...@chromium.org, blink-rev...@chromium.org, blink-...@chromium.org, kinuko...@chromium.org, loading-rev...@chromium.org

        Chromium LUCI CQ submitted the change with unreviewed changes

        Unreviewed changes

        16 is the latest approved patch-set.
        The change was submitted with unreviewed changes in the following files:

        ```
        The name of the file: third_party/blink/renderer/core/html/parser/html_document_parser_test.cc
        Insertions: 19, Deletions: 11.

        The diff is too large to show. Please review the diff.
        ```
        ```
        The name of the file: third_party/blink/renderer/core/html/parser/html_tree_builder.cc
        Insertions: 57, Deletions: 7.

        The diff is too large to show. Please review the diff.
        ```
        ```
        The name of the file: third_party/blink/renderer/core/html/parser/html_tree_builder.h
        Insertions: 3, Deletions: 0.

        The diff is too large to show. Please review the diff.
        ```

        Change information

        Commit message:
        Fixed HTML parser performance for large inline scripts/styles

        This changes the HTML tree builder to throttle flushes while it is in
        text mode (script,style, textarea, etc). This avoids a O(n^2) string
        copy on every flush. The string itself is already being accumulated so
        the flush is unnecessary and it will be handled when the token ends
        (explicitly with an end tag or implicitly with the EOF).

        The flushes follow an exponential backoff where the first flush is
        allowed to happen immediately, then they are blocked for 125ms and
        the interval doubles each time a flush happens up to a 4-second
        maximum. That maintains the performance benefits while still
        providing incremental rendering for massive text blocks.

        This fixes a pathological case where huge scripts or other opaque
        strings can take much longer to parse on Chrome than other browsers.
        Specifically, the test case referenced in the linked issue drops
        from 14.5s to 3.5s (matching Firefox).
        Bug: 500385603
        Change-Id: I3ad0551aae691de9be6bb66230a8a88dcd38917d
        Reviewed-by: Mason Freed <mas...@chromium.org>
        Commit-Queue: Patrick Meenan <pme...@chromium.org>
        Cr-Commit-Position: refs/heads/main@{#1616870}
        Files:
        • M third_party/blink/common/features.cc
        • M third_party/blink/public/common/features.h
        • M third_party/blink/renderer/core/html/parser/html_document_parser_test.cc
        • M third_party/blink/renderer/core/html/parser/html_tree_builder.cc
        • M third_party/blink/renderer/core/html/parser/html_tree_builder.h
        Change size: L
        Delta: 5 files changed, 302 insertions(+), 3 deletions(-)
        Branch: refs/heads/main
        Submit Requirements:
        • requirement satisfiedCode-Review: +1 by Mason Freed
        Open in Gerrit
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: merged
        Gerrit-Project: chromium/src
        Gerrit-Branch: main
        Gerrit-Change-Id: I3ad0551aae691de9be6bb66230a8a88dcd38917d
        Gerrit-Change-Number: 7737055
        Gerrit-PatchSet: 19
        Gerrit-Owner: Patrick Meenan <pme...@chromium.org>
        Gerrit-Reviewer: Chromium LUCI CQ <chromiu...@luci-project-accounts.iam.gserviceaccount.com>
        Gerrit-Reviewer: Mason Freed <mas...@chromium.org>
        Gerrit-Reviewer: Patrick Meenan <pme...@chromium.org>
        open
        diffy
        satisfied_requirement
        Reply all
        Reply to author
        Forward
        0 new messages