theKashey / used-styles

πŸ“All the critical styles you've used to render a page.
MIT License
137 stars 9 forks source link

Serializable lookup table #66

Closed AlexandrHoroshih closed 2 months ago

AlexandrHoroshih commented 2 months ago

Hello!

Problem

Currently used-styles expects the user to generate a lookup table on server startup, by scanning the project's css files.

This leads to a bunch of problems like issues with special environments #40 or having to load the client css chunks into the final container with the server application, which affects its size and generally makes things a bit more complicated.

Suggestion

What do you think about making the lookup table serializable?

That way you could generate styles-lookup.json while building the application and load it into any environment, similar to how it works for @loadable/component and its loadable-stats.json πŸ€”

Something like this

During build process

Lookup table is generated during build step.

// project/scripts/generate_critical_css_lookup.mjs
import { discoverProjectStyles } from 'used-styles'
import { writeFileSync } from 'node:fs'

const serializableLookup = await discoverProjectStyles('./path/to/dist/client', { serializable: true })
writeFileSync('./path/to/dist/server/styles-lookup.json', JSON.stringify(serializableLookup))
yarn build:client
node ./scripts/generate_critical_css_lookup.mjs

Server runtime

The server runtime only requires styles-lookup.json to handle critical styles, and client-side css is not required to run the server, which opens up the possibility of using the library in cases like #40 (i.e. when client-side css files near the server are unavailable or undesirable), since serializable lookup table can be injected anywhere.

In case of a normal server it can be read from disk

const stylesLookup = await fs.readFile('./styles-lookup.json')

app.use('*', async (req, res) => {
  try {
    // ...
    const styledStream = createCriticalStyleStream(stylesLookup); // <style>.myClass {...

    await renderApp({ res, styledStream });
  } catch (err) {
    res.sendStatus(500);
  }
});

In cases like #40 it could be injected directly into final bundle, before deployment.

Interestingly, the current lookup is in fact already serializable, the only problem is that it is represented as a special thenable object πŸ€” I did a small experiment here in which i managed to make it work with a little monkey-patching πŸ˜…

@theKashey What do you think about this feature?

If you ok with that, i would be happy to work on a PR However, I don't have a specific vision for the API yet, so I'd like to discuss that as well.

AlexandrHoroshih commented 2 months ago

However, I don't have a specific vision for the API yet, so I'd like to discuss that as well.

My current idea is to return a Promise<SerializableStyleDef> instead of current thenable StyleDef, if it is specified with configuration, something like this

const serializableLookup = await discoverProjectStyles('./path/to/dist', { serializable: true })

where

type SerializableStyleDef = {
 urlPrefix:string
 lookup: Readonly<StylesLookupTable>;
 ast: Readonly<StyleAst>;
 serializable: true
}

This SerializableStyleDef then could be consumed by all current scanner APIs in place of original StyleDef - scanners should only check, if it is a "thenable" value (need to ensure, that it is ready) or serialized one (it was created during build and there is no need to wait for anything)

πŸ€”

theKashey commented 2 months ago

Your idea would help with a few cases of mine as well πŸ‘

AlexandrHoroshih commented 2 months ago

Your idea would help with a few cases of mine as well πŸ‘

Good to hear! πŸ˜„ As i said before, i'm ready to implement a PR - what do you think of this approach to API? https://github.com/theKashey/used-styles/issues/66#issuecomment-2191373384

Or, maybe, you had some other ideas in that direction? πŸ€”

theKashey commented 2 months ago

Ideally build a new API on top or aside

//
const lookup = await discoverProjectStyles('./path/to/dist');
writeFileSync('./path/to/dist/server/styles-lookup.json', JSON.stringify(serializeStyles(lookup)))

// 
const styles = loadSerializedStyles(require('./dist/server/styles-lookup.json');

That would be easier to test and easier to use.

AlexandrHoroshih commented 2 months ago

Yeah, this option looks good to me πŸ‘

Will do it then!