-
-
Notifications
You must be signed in to change notification settings - Fork 813
Feat: Allow use of Tailwind JIT engine #4806
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces experimental support for running Tailwind JIT on the server, thereby reducing the CPU and network load caused by the client-side Tailwind library.
- Parameter types allowing tailwind to be set to True/False or 'jit' have been updated in multiple functions and configuration files.
- A caching mechanism and endpoint for serving the Tailwind JIT generated CSS have been introduced, and the template has been updated to load the generated CSS.
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
File | Description |
---|---|
nicegui/ui_run_with.py | Updated tailwind parameter type and documentation to support 'jit'. |
nicegui/ui_run.py | Adjusted tailwind parameter type and updated reload exclusions to avoid restarting the server for generated files. |
nicegui/templates/index.html | Added conditional loading of tailwind_jit css based on configuration. |
nicegui/nicegui.py | Introduced caching for Tailwind JIT CSS generation and added a dedicated API endpoint. |
nicegui/client.py | Updated response payload to include tailwind_jit flags and cache busting value. |
nicegui/classes.py | Updated ALL_CLASSES_EVER_USED to include new classes for cache busting. |
nicegui/app/app_config.py | Updated tailwind parameter type to support 'jit'. |
|
||
|
||
@functools.lru_cache(maxsize=1) | ||
def _get_tailwind_jit_csstext(classes: frozenset) -> Optional[str]: | ||
"""Get the result of the Tailwind JIT compilation for the given classes.""" | ||
global _tailwind_jit_call_count # noqa: PLW0603 | ||
_tailwind_jit_call_count += 1 | ||
if _tailwind_jit_call_count > 1: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider reviewing the use of the global _tailwind_jit_call_count within the cached function for potential thread-safety issues if the function is ever called in a concurrent context.
@functools.lru_cache(maxsize=1) | |
def _get_tailwind_jit_csstext(classes: frozenset) -> Optional[str]: | |
"""Get the result of the Tailwind JIT compilation for the given classes.""" | |
global _tailwind_jit_call_count # noqa: PLW0603 | |
_tailwind_jit_call_count += 1 | |
if _tailwind_jit_call_count > 1: | |
_tailwind_jit_lock = asyncio.Lock() # lock to ensure thread-safe access | |
@functools.lru_cache(maxsize=1) | |
def _get_tailwind_jit_csstext(classes: frozenset) -> Optional[str]: | |
"""Get the result of the Tailwind JIT compilation for the given classes.""" | |
global _tailwind_jit_call_count # noqa: PLW0603 | |
async with _tailwind_jit_lock: # ensure thread-safe access | |
_tailwind_jit_call_count += 1 | |
if _tailwind_jit_call_count > 1: |
Copilot uses AI. Check for mistakes.
'tailwind': core.app.config.tailwind, | ||
'tailwind': core.app.config.tailwind is True, | ||
'tailwind_jit': core.app.config.tailwind == 'jit', | ||
'tailwind_jit_cachebusting': hash(frozenset(ALL_CLASSES_EVER_USED)) if core.app.config.tailwind == 'jit' else None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using Python's built-in hash may result in non-deterministic values between sessions; consider using a stable hash function (e.g., hashlib.md5) to ensure reliable cache busting.
'tailwind_jit_cachebusting': hash(frozenset(ALL_CLASSES_EVER_USED)) if core.app.config.tailwind == 'jit' else None, | |
'tailwind_jit_cachebusting': hashlib.md5(str(sorted(ALL_CLASSES_EVER_USED)).encode()).hexdigest() if core.app.config.tailwind == 'jit' else None, |
Copilot uses AI. Check for mistakes.
Thanks @evnchn. While performance improvements are great, they come at the cost of added complexity. This PR looks not to complicated but still it will add a maintainability burden. Besides that I have two questions for now:
|
I think that, yes, it will add a maintainability burden, but I think it can be partially mitigated with a "beta rollout" strategy, in that NiceGUI does NOT take responsibility if the Tailwind JIT grinds to a halt, until we have enough brave users writing in that they have good experience with the feature, then we mark this as stable. Consider that, if you think this is too risky. While for your points:
|
I want to emphasize that, no matter how slow Since, given the Therefore, it seems that for serving large websites, the pros outweight the cons. Notably, I tried it with NiceGUI website, and it works flawlessly to my eyes. Only case which this PR breaks: Creating elements mid-execution ( |
On a single page this might only happen when dynamically adding content. But the re-run must also happen if a new page is first accessed which contains not-before-used tailwind classes.
Why are the benefits bigger for large websites? The performance boost for smaller sites should be more significant because the full tailwind js takes up a bigger portion of the overall page, or not?
Yes in such cases the developer should be warned. I have still not found the time to try it myself. Maybe that would help me to decide whether I agree with your overall feeling that "the pros outweight the cons". |
Yes you are right. But I am thinking if potentially a solution similar to #4799 (decorator which executes the page function right away the first time around) can eliminate this issue. Then, we can collect all classes and run the JIT once. I am thinking about a more explicit API: ui.tailwind_jit('font-bold') # registers the class for tailwind JIT
ui.label().tailwind_jit('font-bold') # this time around, we throw an error if unregistered classes are used
You have to bear in mind, that the tailwind client-side is a JS file, and code execution goes on. While chatting with my friend about this PR, we noticed something: Chunk marked in red: Vue (and possibly Quasar) Chunk marked in blue: Definitely Tailwind CSS JS So, we can visually see the speedup. Applying this to a larger website, like NiceGUI documentation: We can drop these blue chunks from the loading time, which can be huge as well. Bottom line, I think trying this would make it more obvious how the pros and the cons are, and which outweights which (or it's a 50:50). But, right now, it is designed for Windows only, since it expects a I think it could be possible for Linux and macOS users to rename their tailwind CLI standalone installation to a file ending in |
Ok, I see the potential benefit. But, the three main questions I have at the moment (apart from added complexity):
|
Don't get me wrong, I love to squeeze out every bit of speedup for NiceGUI! A while ago I found this project: Completely different from this PR but just a thought about tailwind speedup. |
@EmberLightVFX Surprise to no one, they are also calling the Tailwind Standalone executable. Not sure if we can use that library to make our work easier, but eventually I guess if we want JIT we'll use that executable one way or another. |
Another idea I want to share: would it be possible to load all tailwind classes progressive? I mean:
|
Now I'm not saying that JIT can definitely solve it, but: ![]() Addressing #4822, note how every animation frame, Tailwind client-side JS does something with its MutationObserver |
Ok, to summarize where we're at: We're looking for a way to convert HTML classes to CSS definitions, ideally
This is basically what we're already doing - up to point 4. The current TailwindCSS is pretty large and costly. So we're thinking: Maybe there are low-weight alternatives like Twind, UnoCSS, Windi CSS, or Master CSS? If we can live without some features (like plugin support), we could switch in 3.0 (or let the user choose between two Tailwind implementations) and we're done. |
Thanks for pitching the other alternatives. I have analyzed them, and tabulated as such.
(WindiCSS is sunsetting, and is similar to UnoCSS, so I am not considering it) JIT and AOT definition between TailwindCSS and MasterCSS
So, TailwindCSS’s JIT = MasterCSS’s AOT, MasterCSS’s JIT is TailwindCSS’s Play CDN mode, and my PR referring to “Tailwind JIT engine” means the former (TailwindCSS's JIT, MasterCSS's AOT) Reviewing the optionsChoice 2: For TailwindCSS JIT (what this PR is about), incremental change is not a guarantee, and is likely not going to work, otherwise MasterCSS will not put its Hybrid Rendering (AOT + JIT) as its selling point. Even if it did work, 300ms is a lot (assuming cold start), and keeping the executable in memory may be tricky. I don’t think lack of support for incremental change and demanding pre-definition of all styles is a deal-breaker, but it’s a tough pill to swallow nonetheless. Choice 3: Twind looks very interesting, since it gets rid of the Mutation Observer by having you wrap classes in Choice 4: UnoCSS is itself a server-side library. Not only that, unlike Tailwind CSS, it does not have a standalone executable, and whoever likes to use it must install via npm or something. This is a deal-breaker and we’re unlikely to choose it. Choice 5: MasterCSS client-side JS will likely suffer from the same performance issue as TailwindCSS JS, since it also uses Mutation Observer to observe the DOM tree to look for new classes. Since it’s likely equally as slow and requires new syntax, we’re unlikely to choose it. Choice 6: MasterCSS Hybrid Render looks interesting, but run-time wise, it’ll still be slow since it needs to use Mutation Observer. It is only faster for FCP since the initial load already has the CSS baked in from server, but like UnoCSS there is no standalone executable. In conclusion, we’re unlikely to choose it. So far, this leaves Choice 3 Twind as a strong contender, Choice 2 Tailwind JIT engine if you can deal with pre-defining all your classes / try figure out incremental diffing. See if you agree. Thanks! |
UnoCSS can also run on the client: https://unocss.dev/integrations/runtime |
Ah, I see. But it does it by "detect the DOM changes and generate the styles on the fly", and I'd bet Mutation Observer was used somewhere. At the end of the day, we'd like to move away from Mutation Observer, because it's slow, as seen #4806 (comment) (initial page load) and #4806 (comment) (CSS animations and anything which frequently changes the DOM in some way that the Mutation Observer would pick up) |
Unfortunately, Twind seems to be pretty stale (last commit 3 years ago). 😕 |
@rodja I took another look at the UnoCSS runtime at https://unocss.dev/integrations/runtime Even though it may not immediately make things super fast by dropping all client-side computation, it does grant us a lot more control as we can controls the preset:
I'll tend to prioritize investigating that, over Twind (stale) and Tailwind JIT engine (has its limitations) |
Not considering Twind since it is stale. Notably, Let's wait for unocss/unocss#4700, since I think Tailwind V4 is absent from UnoCSS Runtime Preset, since I don't quite feel comfortable jumping on the UnoCSS train, when it doesn't have Tailwind V4 which we will upgrade to in NiceGUI 3.0 eventually. |
UnoCSS's results are out in #4832 By now it is a draft PR, you can imagine it didn't go too well, then. Quite a lot of things break, and it is by no means a painless transition. Let's discuss UnoCSS from that thread going forward! |
I know this sounds dumb at first, but hear me out. NiceGUI uses TailwindCSS V3 since v1.0.0, as far as I can tell. Before then, it was using some sort of f6194c0#diff-4e4cc16e68c714c6a8433c24c00c6bc837f4f7ed51259f6e4bff356931927ed0 Takeaway: For performance-focused crowds, they can probably get away with a performance uplift, if they turn off Tailwind via Besides limited features, I don't see much downsides. CSS isn't a programming language, so if it's old we won't get any security vulnerabilities or anything. If anything, browser support is even better. |
Oh wow, that's an important insight! I wasn't aware (or I forgot) that we switched from a CSS file to JS in 1.0.
https://tailwindcss.com/blog/tailwindcss-v3?utm_source=chatgpt.com#play-cdn I wonder if we could auto-generate our own tailwind.css with all well-known classes from tailwind.py? Then the user can choose between full or limited Tailwind support. Unfortunately the "limited" version would lack quite a lot of useful features. |
I just swapped out NiceGUI documentation's Tailwind from V3 (JS) to V2 (CSS) Many stuff breaks. Namely
And so, our I am in the process of seeing what happens when I convert my project to Tailwind V2 CSS. See how many syntax changes I need to make. |
Would like to focus effort on UnoCSS (#4832) instead of this. I can't find UnoCSS on the performance timeline but I think it should be really fast compared to Tailwind client-side library already. UnoCSS: ![]() TailwindCSS client-side library: ![]() |
Motivation
As discovered in #4802, the client-side Tailwind JavaScript library is CPU resource intensive (and network resource intensive, if you don't consider cachine), taking up over 60% of the page load time.
Provided that the user compromise by declaring classes they want to use ahead-of-time, it was discovered that we can run the Tailwind JIT engine on the server, significantly speeding up the JavaScript execution needed to get the page ready and interactable.
Implementation
tailwind='jit'
forui.run()
andui.run_with()
, alongside existingTrue/False
ALL_CLASSES_EVER_USED
, which is used for:tailwind.jit.css
tailwind_jit
andtailwind_jit_cachebusting
to theindex.html
template, and display accordingly.tailwindcss.exe
as downloaded in https://v3.tailwindcss.com/docs/installation in the NiceGUI storage folder (default.nicegui
for processingProgress
.exe
suffix, after all) @falkoschindler do you think you can do that ASAP so that you can test? Or otherwise I need to get my Chromebook ready...--watch
mode for faster Tailwind JIT completion times, eliminating the cold start?tailwind.jit.css
as critical CSS (inlined inindex.html
)? (I think not, since we have cachebusting and aggressive cache... Uncheck if you'd like to discuss)Performance uplift
As from #4802 (comment)