Skip to content

Conversation

evnchn
Copy link
Collaborator

@evnchn evnchn commented Jun 3, 2025

Motivation

As in #4806, UnoCSS is one possible contender, such that we can move away from the monolithic, large, and slow Tailwind client-side JS.

Implementation

  • Enroll UnoCSS into npm.json (except tailwind.css style reset CSS, that one to be done later)
  • Load in UnoCSS if Tailwind is disabled (for now, later we'll have a dedicated switch for UnoCSS in ui.run I'd imagine
  • Use UnoCSS in the NiceGUI documentation website to see what breaks

Progress

  • I chose a meaningful title that completes the sentence: "If applied, this PR will..."
  • The implementation is complete.
  • Pytests have been added (or are not necessary).
  • Documentation has been added (or is not necessary).

Some tasks:

  • Enroll and assess tailwind.css Tailwind Style Reset
  • Fix Flash of Unstyled Content
  • Make configurable what UnoCSS presets are loaded in

Results

So, as it turns out, UnoCSS is NOT a 100% drop-in replacement for Tailwind CSS. Documentation says: "It should be noted that complete compatibility may not be guaranteed."

Quite a lot of things break.

{9C2D0EDB-01FC-42F8-89AF-9739CBB5DB03} {D4CECE6E-0AB0-4A32-B05B-FEEEDB6B1704}

But, overall, I'd call it "80% of the way there".

Question for discussion

  1. Since UnoCSS doesn't quite work as 100% drop-in replacement, do we want to put more emphasis to the Tailwind JIT engine idea in Feat: Allow use of Tailwind JIT engine #4806
  2. Or, do we let the user deal with it and update their code to use the new UnoCSS syntax (that'd be a breaking change)
  3. Or both?

@evnchn evnchn marked this pull request as draft June 3, 2025 01:14
@rodja
Copy link
Member

rodja commented Jun 3, 2025

I would not give up hope so early. Because the current documentation website is build using Tailwind 3, I strongly suggest to use wind-3 for verification. And if the UI is still broken it would be very interesting which classes are not supported. And why.

But still with the current 80% solution we should be able to make a guess about it's performance improvement. Can you run the numbers @evnchn?

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 3, 2025

I'm using wind3 right now.

I'll see the performance later.

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 3, 2025

Testing on http://127.0.0.1:8080/documentation/section_text_elements, CPU mid-tier mobile, no network throttling.

Note that the gains are going to be smaller than what we see in #4806 (comment), since the page itself is more complex.

UnoCSS Runtime

{C5EA3969-6D01-4F4B-A495-779DD94A44A5}

7092ms, of which 4885ms is UnoCSS Runtime (68.88%)

Tailwind client-side JS

{18861104-B597-4BAF-AFC0-0DEB874580E7}

2400ms, of which 405ms is Tailwind client-side JS


So I think the idea of UnoCSS is not going to work...

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 3, 2025

Before you ask, preset-mini is equally slow

{177A2524-E973-409A-B109-0AFFFBE98657}

I don't think UnoCSS is really built for client-side speed-ups, since it has to be customizable...

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 3, 2025

Might as well also check out Twind, then.

Styles are more broken

{85C9DD71-3E90-4FAE-A5C9-CCA15FE34526}

But it sure is fast, though

{456B2498-3F46-49E9-A882-971173ACD59B}

Testing on /other_page

Tailwind client-side

{81ECF1E1-1BB6-457E-B2D9-C70BADE5965D}

Twind:

{5362DE7E-C195-45FF-978F-F9449FE012D1}

UnoCSS:

{E8FB39B7-57B5-4F1C-B3A0-24968091B42A}
  • If time is y=mx+c, where x is the page size, Tailwind client-side JS has a high constant c, but low slope m
  • Twind is fast, but its even lower compatibility is a deal-breaker.
  • Why is UnoCSS fast in small page, but slow in large page? Did they have a O(n^2) hiding somewhere in their codebase?

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 3, 2025

Huh. I'd classify it to be a bug of UnoCSS, but I don't know...

{7F16F7FE-B467-4F54-A83C-03BF7A9517E9} {FE5E70BC-7B98-4F78-8EBE-BBDF2DB0542D}

Profiling reveals the slowest line being e = e.replace(i, a);

I found it was replacing over the entire document again and again.

So, the longer your document, the more time it will take! O(n^2)!

Original function:

function Kn(e, t, r, n) {
    for (const o of e.matchAll(r)) {
        if (o != null) {
            const i = o[0];
            const a = `${n}${Ll(i)}`;
            t.set(a, i);
            e = e.replace(i, a);
        }
    }
    return e;
}

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 3, 2025

Went with the unminified version:

{48962AEC-AF47-477F-9426-83EDEFFDCC33}

Slow function is at https://github.com/unocss/unocss/blob/main/virtual-shared/integration/src/utils.ts#L23

User of said function is at https://github.com/unocss/unocss/blob/main/packages-presets/extractor-arbitrary-variants/src/index.ts#L15

But staring at it, I don't see anything wrong necessarily...

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 3, 2025

So apparently UnoCSS detect the DOM changes and generate the styles on the fly by listening to the entire body, and then basically regex-ing all the way through.

This is because UnoCSS supports some custom syntax such as attributify and tagify, and that would be impossible to do with just a Mutation Observer listening for class changes, I'd imagine.

So I don't think we should consider the UnoCSS runtime from this point on, since the technical direction is incompatible.

@rodja
Copy link
Member

rodja commented Jun 4, 2025

I think you can do it without the mutation observer by only using the core package (see https://unocss.dev/tools/core#usage). The generate function can be passed a list of classes for which to generate the css.

Or you disable observing window.__unocss = { observe: false } and update the styles of a subtree by calling window.__unocss_runtime.update.

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 6, 2025

  1. If we use only the core package, then there are no rulesets for Tailwind, and it'd do nothing.
  2. window.__unocss = { observe: false } doesn't seem to be a valid option. Where did you read about it?
  3. I also tried window.__unocss_runtime.toggleObserver(false), still observes and page load takes a long time.

@evnchn evnchn marked this pull request as ready for review June 6, 2025 19:48
@evnchn
Copy link
Collaborator Author

evnchn commented Jun 6, 2025

So, got UnoCSS working in 7d49615

  1. Figured out the trick runtime: {ready: () => false} from https://github.com/unocss/unocss/blob/a2acc4ca9dd56f8ac06f25b2da010c74853fabed/packages-integrations/runtime/src/index.ts#L55-L58. I can't find any docs and so this is what I resorted to...
  2. Since we know UnoCSS works on the entire HTML from UnoCSS Support #4832 (comment), we make a placeholder HTML which we populate with just the classes for UnoCSS to work on, sidestepping the speed issue of acting on the entire body.
  3. Based on the above, implement generateStylesFromClasses
  4. UnoCSS actually isn't broken. Just that Quasar's CSS overrides UnoCSS's ones. By moving UnoCSS's definition after Quasar's (by forcing it to be the last element in <head>, we let UnoCSS take priority, and the NiceGUI documentation page is indistinguishiable between Tailwind / UnoCSS now.

@evnchn evnchn changed the title [WIP] UnoCSS Support UnoCSS Support Jun 7, 2025
@evnchn evnchn added the feature Type/scope: New feature or enhancement label Jun 7, 2025
@rodja
Copy link
Member

rodja commented Jun 8, 2025

Glad you found a way to disable the global mutation observer. My research was too LLM biased. Sorry.
How much faster is it? Will it make #4802 and #4806 obsolete?

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 8, 2025

Well, on the 4 metrics as listed #4806 (comment), I think this PR answers YES to all 4 of them.

However, there is one issue: if we add classes via JavaScript, then it won't be caught by nicegui.js, and thus the Tailwind class won't show up. But that's a pretty niche use case if you ask me.

I still think perhaps I should finish up the rest of the tasks, such as fetching the Tailwind reset CSS, allow configure which UnoCSS to load (since preset-mini should be faster and lighter), as well as separating out the option in ui.run so that we don't leverage tailwind=False as a hidden switch for UnoCSS

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 8, 2025

Want to drop some presets to keep the PR small and to avoid confusion.

  • Include @unocss/preset-mini The minimal but essential rules and variants
  • Include @unocss/preset-wind3 Tailwind CSS / Windi CSS compact preset
  • Include @unocss/preset-wind4 Tailwind4 CSS compact preset
  • Definitely Dropping @unocss/preset-attributify Enables Attributify Mode for other rules
  • Definitely Dropping @unocss/preset-tagify Enables Tagify Mode for other rules
  • Web Request @unocss/preset-icons Pure CSS Icons solution powered by Iconify
  • Web Request @unocss/preset-web-fonts Web fonts (Google Fonts, etc.) support
  • Need assessment @unocss/preset-typography The typography preset
  • Need assessment @unocss/preset-rem-to-px Converts rem to px for utils

Comments

  • No attributify and tagify, because setting individual classes via .classes('...') is by in large easier than applying in batch using .props('bg="..."') (nest quotes in your strings) and ui.element('...') (could throw up Vue, and can only apply one class only). Moreover, it means we need to do more than copy the classes over, when we make the placeholder div.
  • preset-icons and preset-web-fonts could possibly fetch from internet, which NiceGUI prefers packing everything into the library if possible
  • preset-typography could mess up Quasar / NiceGUI elements when inside an element with prose class appled
  • preset-rem-to-px could also mess up Quasar / NiceGUI elements. Also it doesn't seem beneficial to me. rem is more flexible I heard?

Do tell me what you think!

@falkoschindler falkoschindler added the in progress Status: Someone is working on it label Jun 10, 2025
@rodja
Copy link
Member

rodja commented Jun 10, 2025

@evnchn I do not know any of these presets. I would suggest to only support the current Tailwind 3 classes as close as possible. If all goes well we can add other helpful / non-harmful presets in later PRs.

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 11, 2025

  • Allow only one UnoCSS preset only, since we are choosing between mini, wind3 and wind4, which when used together, will probably grind to a halt.
  • Added tailwind.css, not the tailwind-compat.css, since the original TailwindCSS client-side library does it in the way of the first one, so we'd like to match it
  • Beat dark mode into working. Tested on documentation website.
  • Fix the test

From how I look at it, besides documentation, it's pretty ready.

Note that preset-wind4.js is built by me, since upstream doesn't have it yet. Also when they change the preset-wind.js to preset-wind3.js, we'd have to follow suit.

@evnchn
Copy link
Collaborator Author

evnchn commented Jun 11, 2025

I'd consider it done by now. It's been a ride!

@falkoschindler falkoschindler added review Status: PR is open and needs review and removed in progress Status: Someone is working on it labels Jun 11, 2025
@falkoschindler falkoschindler self-requested a review June 11, 2025 19:43
@falkoschindler falkoschindler added the 🟠 major Priority: Important, but not urgent label Jul 8, 2025
@falkoschindler falkoschindler modified the milestones: 3.0, 2.x Jul 8, 2025
Copy link
Contributor

@falkoschindler falkoschindler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When planning the upcoming 3.0 release, we thought about switching from Tailwind to UnoCSS. Therefore I started reviewing this PR to see if we want to merge it before 3.0. Here are my thoughts:

  • When reloading the documentation page, the drawer randomly slides in or is shown as an overlay (like on narrow screens). It looks like the screen width isn't detected reliably.
  • Why do we need to wait(1) in the pytest? Is it because we need to wait for UnoCSS to run?
  • Why do we need to set app.config for each page in the pytest? Shouldn't it be enought to set it once for the whole (test) app?
  • Dark mode switching works by checking a condition 10 times per second. Is there a chance to do this more efficiently (without an interval)? Fixed in 9625a1e.
  • At the moment we always track a set of classes, even if not using UnoCSS. Can this be improved?
  • "Adding classes via JavaScript will not be captured by NiceGUI and those classes will not be applied." → This is a hard limitation if we want to replace Tailwind with UnoCSS in 3.0. If there's no (cheap) solution, we might at least need a public function to trigger a CSS update programmatically.

@falkoschindler
Copy link
Contributor

I just replaced the dark mode interval with an event listener. Seems to work nicely.

On the other hand I noticed that Tailwind classes are not only ignored when set via JavaScript, but also e.g. in templates for scoped slots:

ui.table(rows=[{'ID': 'Alice', 'age': 18}, {'ID': 'Bob', 'age': 21}]).add_slot('body-cell-age', '''
    <q-td key="age" :props="props">
        <q-badge :class="props.value < 21 ? 'bg-red-500' : 'bg-green-500'">
            {{ props.value }}
        </q-badge>
    </q-td>
''')

And you can't easily call some restyle() function on change because there isn't even an obvious change event. This is quite a downside compared to the current implementation.

Maybe we find a way to use some kind of efficient observer, maybe only observing individual elements or slots?

@evnchn
Copy link
Collaborator Author

evnchn commented Jul 17, 2025

Oh I am SO busy.

I think UnoCSS is a worthy change. However, the slot case was an oversight of mine.

Eventually, we need to move away from a mutation observer, but right now, for 3.0, I think we really need to get UnoCSS working more than anything (or else we use Tailwind V4 client-side JS...)

So, for the time being, an efficient mutation observer (NOT the one built-into UnoCSS since that observes and parses the entire HTML, which is taking a long time)

When I have time, I can work on an observer-free implementation, and it will be a "speed improvement"

TL-DR: Getting UnoCSS is an important feature, while my insist on not using observer is a speed improvement. Since we can't do the both given my limited time available, we have no choice but to focus on the first one.

@falkoschindler
Copy link
Contributor

I just agreed with @evnchn to postpone this PR to after version 3.0. Since UnoCSS won't replace Tailwind any time soon, we're going to introduce it as a non-breaking opt-in feature anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Type/scope: New feature or enhancement 🟡 medium Priority: Relevant, but not essential review Status: PR is open and needs review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants