Skip to content
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

Unify various modules and helpers #157

Closed
lukejacksonn opened this issue Mar 24, 2021 · 16 comments
Closed

Unify various modules and helpers #157

lukejacksonn opened this issue Mar 24, 2021 · 16 comments
Labels
💡 Proposal Request for comments

Comments

@lukejacksonn
Copy link

lukejacksonn commented Mar 24, 2021

What

Twind started as a single module that exported setup and tw. As the project has evolved we have added helpers like css/apply/style and other features to support things like the shim, SSR and CLI. Now we are coming up to V1 we are looking to clean up and consolidate the Twind offering somewhat.

The general idea here is to create an interface something like this for the client and server respectively:

import { setup, tw, css, apply, style, sheets } from "twind";
import { setup, extract, sheets } from "twind/server";

Why

The aim is to make things a little more intuitive, improve discoverability and make documentation easier. We have witnessed developers struggling to know what to import where and wondering whether they are doing it right. The proposal aims to create just one way to do things.

Considerations

Bundle Size

Unifying modules like this is likely to increase the overall bundle size by a couple of KB (from ~14 to 16). Time taken to download/parse Twind is always a primary concern but we think the pros outweigh the cons here. Another thing to note is that the module will be tree-shakeable so if you are using a bundler and don't use css for example, then it won't be bundled into your application.

Shim by default

You might notice that the shim is not exposed by the proposed interface. The more we have used Twind ourselves or seen it been used by others, we have noticed that the shim is the easiest and most popular mechanism for both server and client. Going forward we would like to make this the default behaviour and have things just work. This means that the go to way of adding styles to your app will be via class="bg-black text-white" which comes with the advantage of aligning static HTML and virtual DOM implementations. We appreciate that not every application will require the runtime component which activates the MutationObserver (and allows for the better dev tools experience) so we are proposing that this can be disabled by config similarly to the autoprefixer and hashing setup({ runtime: false }).

Tailwind Utilities

By default Twind includes Tailwind theme/variants/utilities definitions because it is a comprehensive and well documented preset. However this is where the vast majority of Twind's filesize comes from. Something we have been considering lately is abstracting these out to twind/tailwind which make Twind core offering very obvious and promote the idea of developers/companies to create their own design system that they can use with Twind much like we do by default with Tailwind as a basis today. This might also allow developers to create custom builds that only import the utilities they need resulting in smaller bundles for everyone.

The downside of this however is (quite contrary to the primary goal here) that it would requires something like this:

import { setup, tw, css, apply, style, sheets } from "twind";
import * as tailwind from "twind/tailwind"

setup({ presets: [{ ...tailwind }] });

With that said, this is probably something we could either a) do internally but still apply Tailwind presets by default or b) not do at all c) embrace the ideal wholly knowing that extra import only has to be done once during setup.

RFC

I'm creating this issue to gather feedback, thoughts and further considerations that we might not have been noted here. If you have and bright ideas or strong opinions on any aspect of this proposal then please comment below and I will update this description accordingly.

/cc @tw-in-js/contributors

@danielweck
Copy link
Member

danielweck commented Mar 26, 2021

Unifying modules like this is likely to increase the overall bundle size by a couple of KB (from ~14 to 16).

Unless I am mistaken, I think that the advertized 14-16KB footprint is slightly misleading. There are several other CSS-in-JS libs that claim "near-zero" / "tiny" runtime, sometimes optimistically. In contrast, Twind offers complete Tailwind CSS support (and more) for a relatively small price, which I believe is worth documenting more openly.

Let's break it down:

I believe the currently-advertized bundle size corresponds to the "minimal" Twind runtime which excludes the style-vendorizer import (external), and also excludes the separate package entries: twind/colors, twind/css, twind/observe, twind/server, twind/sheets, twind/shim, twind/shim/server, twind/style. The quoted size represents the single file / minified ES Module (i.e. multiple exports for the entire "minimal" Twind API), with HTTP gzip compression (not Brotli? EDIT: recently introduced: 7d9cc19 ).

https://bundlephobia.com/[email protected]

https://unpkg.com/browse/[email protected]/

Now, in my pet project (i.e. a small performance-oriented Preact WMR website), the Rollup-bundled and Terser-minified JS chunk dedicated to Twind weighs in at 35KB without any plugins. The actual practical size is 53KB with the "typography" and "forms" plugins (and I plan to use other ones too). These KB figures are on-disk sizes, before gzip compression. Note that I use twind/style and twind/css (2KB each), and at some point I will probably use twind/colors too (+ 3KB). PS: I have a separate chunk for static SSR, which bundles twind/server and twind/sheets (transitively).

The 50+KB (pre-compression) Twind-dedicated JS bundle is actually never loaded in the frontend of my production builds, but I suspect that I am an exception to the rule: the vast majority of Twind users / developers will ship the runtime, which is; all things considered; relatively lightweight given Twind's rich feature set. I just don't think 16KB (~30KB pre-compression) is achievable in real-word use.

EDIT: added size-limit constraints:

twind/package.json

Lines 42 to 84 in 7d9cc19

"size-limit": [
{
"name": "twind",
"path": "dist/twind.js",
"brotli": true,
"limit": "12kb"
},
{
"name": "twind/colors",
"path": "dist/colors/colors.js",
"brotli": true,
"limit": "1kb",
"ignore": ["twind"]
},
{
"name": "twind/css",
"path": "dist/css/css.js",
"brotli": true,
"limit": "1kb",
"ignore": ["twind"]
},
{
"name": "twind/observe",
"path": "dist/observe/observe.js",
"brotli": true,
"limit": "0.7kb",
"ignore": ["twind"]
},
{
"name": "twind/shim",
"path": "dist/shim/shim.js",
"brotli": true,
"limit": "0.3kb",
"ignore": ["twind"]
},
{
"name": "twind/style",
"path": "dist/style/style.js",
"brotli": true,
"limit": "0.8kb",
"ignore": ["twind"]
}
],

@danielweck
Copy link
Member

danielweck commented Mar 26, 2021

Shim by default: we are proposing that this can be disabled by config similarly to the autoprefixer and hashing setup({ runtime: false }).

Yes please 👍 :)

@danielweck
Copy link
Member

Tailwind Utilities:

setup({ presets: [{ ...tailwind }] });

That would be great!

I only had a superficial look at the core lib's source code, so I don't have a clear picture of how costly it would be to factor out the Tailwind definitions (in terms of development effort). But if you guys feel you can do it, I will totally root for you ;)

I wonder if Twind plugins could be harmonized based on the same architectural principles (i.e. declarative CSS definitions + small integration API), and whether or not this could help power the typescript language service (see tw-in-js/typescript-plugin#8 )

It would be nice if there was a default setup() which imported all TW definitions (just like now), and a separate setupMini() for advanced usage (e.g. import partial TW definitions from Twind's preset, or an entirely different preset altogether, or mixed / merged).

Regarding tree shaking / dead code detection: I don't have a clear idea of how granular the TW definitions are / can be in Twind's code, but once factored out it should be possible to identify atoms of functionality that can safely be excluded from production JS bundles.

Seems like a ton of work, but if you guys can do it :) 👍

@sastan
Copy link
Collaborator

sastan commented Mar 26, 2021

@danielweck We are talking about compressed file sizes here. In one of the latest commits I've updated size-limit to show some more stats (the sub-modules do not include twind as dependency):

Bildschirmfoto 2021-03-26 um 16 49 12

I believe that these are the stats we should talk about:

  • size with all dependencies, minified and brotli
  • total time (loading time on slow 3G + running time on Snapdragon 410)

How much raw JS there is not important most of the time. What matters are how much must be parsed and how much is evaluated when importing the module.

These things are reflected in the running time. I know that this is not what people are used to but that is what matters.

That is why we should talk/mention total time (load +eval) and size (minified & brotli).

@danielweck
Copy link
Member

danielweck commented Mar 26, 2021

We are talking about compressed file sizes here.

Of course. I thought this was clear in my comment, sorry if it wasn't.

Looking at Bundlephobia numbers, and then looking at Twind's bundle ESM code in the "core lib", style-vendorizer is external. More importantly, real world applications are very likely to use the other twind/xxx packages, along with common plugins. So my point still stands that the advertised KB is in relation to a very specific context.

I love your updated Brotli figures by the way! :) As expected, better than gzip. Can you please verify that style-vendorizer is indeed included in the tested bundles?

https://bundlephobia.com/[email protected]

@danielweck
Copy link
Member

I believe that these are the stats we should talk about:

* size with all dependencies, minified and brotli

* total time (loading time on slow 3G + running time  on Snapdragon 410)

How much raw JS there is not important most of the time. What matters are how much must be parsed and how much is evaluated when importing the module.

These things are reflected in the running time. I know that this is not what people are used to but that is what matters.

That is why we should talk/mention total time (load +eval) and size (minified & brotli).

I couldn't agree more. Lighthouse screenshots with vanilla HTML+Twind integration would illustrate this quite well (I mean: no (P)React, Vue, Solid, etc.)

Naturally, in a web app with a giant DOM, lots of inline SVG, non-lazy image loading, non-deferred scripts, analytics, ads, etc., the impact of Twind runtime will be relatively minimal :)

@sastan
Copy link
Collaborator

sastan commented Mar 26, 2021

These results are to be expected because the shim kicks in after DOMContentLoaded.

For production sites we recommend critical CSS extraction.

I'm currently experimenting with one or two ways to optimize this. One of them is:

<script>
// Inline https://unpkg.com/twind/twind.umd.js
// Inline https://unpkg.com/twind/observe/observe.umd.js

twindObserve.observe(document.documentElement)
</script>

Could you try that one?

@danielweck
Copy link
Member

Could you try that one?

Sure. My current test is with:

  <!-- script type="module" src="https://cdn.skypack.dev/twind/shim"></script -->
<!-- LOCALHOST COPY: -->
  <script type="module" src="./shim.js"></script>

@sastan
Copy link
Collaborator

sastan commented Mar 26, 2021

@danielweck This is quite intersting. Could we move that into discussion or discord.

@danielweck
Copy link
Member

@danielweck This is quite intersting. Could we move that into discussion or discord.

I will do.

@danielweck
Copy link
Member

I created a separate GitHub "discussion": #161

@lukejacksonn
Copy link
Author

Thanks for your comments @danielweck I will follow the conversation on the thread you created (as this was not really the intended topic here) but just to let you know that I'm working on a "not so pet project" (a global ecommerce site) and we only have ~16KB of compressed twind imported at runtime (which includes tw/shim/css/style). All critical CSS is statically extracted during SSR with twind/next.

So in that sense I think that is is reasonably reasonable to state this kind of JS size, excluding of course all the additional "non-essential" modules like typography and forms.

@danielweck
Copy link
Member

Thanks Luke, good to hear about Twind being used in production websites.
In my small-scale tests the main impact of Twind runtime is not so much its total bundle size, but the impact on web vitals on mobile devices from Twind shim/observe, during stylesheet hydration (i.e. reconciliation / merging of SSR prerendered CSS with dynamically-generated class selectors + rules).

I don't use Next.js, and my use case is certainly different. I use Preact WMR (crucially: static SSR, not dynamic route prerendering), so I generate an inline critical / minimal CSS style element for pre-hydration initial render, as well as a separate async external stylesheet for other styles used post-hydration. This way, I am able to completely negate the need for the Twind runtime / just-in-time resolver.

The Twind CLI which extracts styles ahead of time is very useful in many cases, but there are significant caveats (by design, not bugs) which stop me from using this utility. Plus, the WMR pre/post processing plugin pipeline and Preact's VNode interceptor allow me to more accurately extract used Twind CSS.

If you don't mind me asking, do you use Twind's open source Next.js integration, or some in-house solution?

Cheers, Daniel

@lukejacksonn
Copy link
Author

Only just seen this reply so thanks for your patience. We are using @twind/next out of the box with no special configuration. It works absolutely great. The critical CSS is extracted to a style tag at the top of the page on first load SSR.. after that the shim kicks in and handles dynamic styles at runtime (think a modal that was opened by the user after page load that requires styling). This comes with the added benefit of not shipping potentially unused styles to the client, but obviously this is a tradeoff.

I'm still unsure of how valid the concern is of performance degradation when it comes to loading in Twind. On our netlify deploy environments we were seeing lighthouse perf scores ~90 and once we promoted the site to a production like environment we are seeing that go up to ~100 for desktop at least.

I do agree however that it would be interesting to see how we could make hydration more efficient. My initial approach would be to send down a map of already computed rules (essentially a serialized version of the cache that Twind would generate on the server) to prevent duplication of work by the browser. It sounds easy enough but I'm sure it comes with drawbacks/complications that I have not considered.

@bebraw
Copy link
Collaborator

bebraw commented Oct 12, 2021

I've been using Twind in a Deno environment. There's one problem related to this issue in that case which you might want to consider.

At https://unpkg.com/[email protected]/twind.js, there's a dependency on style-vendorizer (i.e. import {cssPropertyAlias, cssPropertyPrefixFlags, cssValuePrefixFlags} from "style-vendorizer";) and that won't work out of box with Deno. To solve, I've defined an alias against it using import maps ("style-vendorizer": "https://unpkg.com/[email protected]/dist/esm/bundle.min.mjs",).

Although this works, it fails when you want to use Twind within a web worker as import maps aren't supported there yet (denoland/deno#6675).

If you re-architect this portion of Twind, maybe a good option here would be to allow the consumers to inject style-vendorizer somehow or at least have it as an optional dependency for the project.

@sastan
Copy link
Collaborator

sastan commented Jan 25, 2022

Implemented in twind v1.

Please give it a try. Here are some links to get you started:

Closing this for now. Feel free to re-open.

@sastan sastan closed this as completed Jan 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💡 Proposal Request for comments
Projects
None yet
Development

No branches or pull requests

4 participants