[Bug]: ComposerBanner's `+ :has()` variant makes banner changes restyle the whole page on every thread switch
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 88/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- tailwindcss, typescript
- Lĩnh vực
- frontend, performance
Hướng nghiên cứu
Bắt đầu trong apps/web/src/components/chat/ComposerBanner.tsx tại dòng 114 và so sánh selector được phát ra với bản tái hiện tối giản bằng Chromium trong issue. Xác minh thay đổi bằng các trường hợp tương đương selector được liệt kê và một Performance trace trong khi bật tắt banner hoặc chuyển thread. Hoàn tất có nghĩa là kiểu của banner vẫn tương đương trong khi việc vô hiệu hóa kiểu trên diện rộng được giảm xuống.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/web
Steps to reproduce
- Build
main(measured at95030dc674) and open the web app in Chromium with a populated sidebar. - Record a DevTools Performance trace while switching between threads.
- Look at the "Recalculate Style" events during each switch.
The minimal reproduction below needs no T3 Code. Open it in Chromium, record a Performance trace, and click the button:
<!doctype html>
<style>
/* ComposerBanner.tsx:114, as Tailwind compiles it */
.X + :has([data-chat-composer-form]) [data-chat-composer-form] > [data-slot=composer-banner-attachment]:first-child [data-composer-banner-surface=attached]::before { border-radius: 0 }
/* any group-has-* utility; its group does not even need to be on the page */
:is(:where(.group\/surface):has([data-banner=attached]) *) { box-shadow: none }
</style>
<button id="toggle">toggle banner</button>
<div id="list"></div>
<div id="stack"><div id="banner" class="X">banner</div><div><form data-chat-composer-form><div data-slot="composer-banner-attachment"><div data-composer-banner-surface="attached"></div></div></form></div></div>
<script>
document.getElementById("list").innerHTML = "<div><span>row</span></div>".repeat(1500);
const stack = document.getElementById("stack");
const banner = document.getElementById("banner");
document.getElementById("toggle").onclick = () => (banner.isConnected ? banner.remove() : stack.prepend(banner));
</script>
Expected behavior
Adding or removing a banner restyles the banner's neighbourhood, as it does when either rule is present alone.
Actual behavior
The Tailwind arbitrary variant in apps/web/src/components/chat/ComposerBanner.tsx:114 compiles to a bare :has() after a sibling combinator:
.X + :has([data-chat-composer-form]) [data-chat-composer-form] > [data-slot=composer-banner-attachment]:first-child [data-composer-banner-surface=attached]::before
When a group-has-* utility is in the same stylesheet (main ships many), adding or removing a sibling in that area makes Chromium restyle almost the whole document. The reproduction above, 20 banner toggles, counted with the trace's UpdateLayoutTree.elementCount:
| Stylesheet in the reproduction | Elements restyled per banner toggle (of 3,013) |
|---|---|
no :has() rules |
1 |
group-has rule only |
1 |
| ComposerBanner rule only | 6 |
| both, as in the file above | 2,861 |
| both, with the suggested rewrite below | 6 |
The group-has rule's group element is not on the page at all; the rule only has to be in the stylesheet.
A thread switch adds and removes these siblings, so every switch pays for it. Measured on a main build (95030dc674, default settings), 13 scripted thread switches in headless Chromium, three rounds each:
| As shipped | Only the two ComposerBanner rules rewritten | |
|---|---|---|
| Recalcs touching ≥1,000 elements | 17 | 1 |
| Elements restyled | 32.3k | 12.4k (−62%) |
| Style recalculation time | 315 ms | 156 ms (−50%) |
| Main thread blocked in long tasks, rapid switching | 728 ms | 461 ms (−37%) |
| Click → next paint, p50 | 74 ms | 59 ms |
| Keydown → next paint, p95 | 54–64 ms | 25–26 ms |
The element counts come from Chromium's own trace and do not depend on machine load; they repeated exactly across rounds.
Suggested fix: remove the :has(), which is redundant. The rest of the selector already requires a [data-chat-composer-form] descendant of that sibling, so + * matches the same elements:
- "[&+:has([data-chat-composer-form])_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:rounded-none [&+:has([data-chat-composer-form])_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:border-t-0",
+ "[&+*_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:rounded-none [&+*_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:border-t-0",
Checked equivalence: computed ::before styles are identical for the old and new selector with the form host after the attachment, with no attachment before it, with the attachment not first, and with the form as the sibling itself. With the rewrite, the reproduction's banner toggle restyles 6 elements instead of 2,861.
Impact
Major degradation or frequent failure
Version or commit
main @ 95030dc674 (rule introduced in #8693)
Environment
Linux, Chromium (headless, for the measurements) and the Electron desktop app. The effect is in Chromium's :has() invalidation, so every Chromium-based client is affected.
Logs or stack traces
The trace shows ScheduleStyleInvalidationTracking with changedPseudo: "has" walking every ancestor up to <html>, followed by StyleInvalidatorInvalidationTracking "Invalidation set invalidates subtree" with allDescendantsMightBeInvalid: true on each of them.
Screenshots, recordings, or supporting files
The reproduction above.
Workaround
None for users. Removing the two classes, or applying the suggested rewrite, removes the effect.
- Ngôn ngữ chính
- TypeScript
- Star
- 24k
- Fork
- 6.3k
- Merge trung bình
- 10 giờ 46 phút
- Pull request đã merge (30 ngày)
- 299
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của pingdotgg/t3code
-
enhancement via-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
pingdotgg/t3code#14541 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
pingdotgg/t3code#14536 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug via-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
pingdotgg/t3code#14452 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của pingdotgg/t3code
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
callstackincubator/rozenite#518 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Area/Workflow Priority/Blocker Type/Bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
wso2/product-integrator#2622 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
-
area:bash bug has repro platform:macos
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
anthropics/claude-code#98644 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
allure-framework/allure-js#1603 ·
Maintainer thường phản hồi trong vòng 1 ngày