vite-plugin-optimize-css-modules icon indicating copy to clipboard operation
vite-plugin-optimize-css-modules copied to clipboard

Class shaker

Open benallfree opened this issue 5 months ago • 1 comments

One thing that surprised me about CSS Modules in Vite is that it doesn't automatically shake unreferenced classes.

.unused {
   font-size: 42px;
}

.used {
   font-size: 27px;
}
import { used } from './foo.module.css'

...

The .unused class will appear in the output.

I noticed that this plugin does not shake unused classes either, so I made a small postprocessor that does the job.

If this seems like a good idea, I wonder if it could be incorporated into this plugin somehow. Basically it scans through the JS files to gather possible class names (/"([A-Za-z_]{1,2})"/g) then uses PurgeCSS with a custom extractor to strip the unused classes.

I think this could be incorporated as a { shake: true } option for this plugin. Shaking would require hooking into the JS output from Vite, marking known class names as used, and then stripping the unused ones before outputting the final CSS.

import { readFileSync, writeFileSync } from 'fs'
import { ExtractorResultDetailed, PurgeCSS } from 'purgecss'

const tags = ['a', 'abbr', ...]

const purgeFromJs = (content: string): ExtractorResultDetailed => {
  const regex = /"([A-Za-z_]{1,2})"/g
  const matches: string[] = []
  let match: RegExpExecArray | null

  while ((match = regex.exec(content)) !== null) {
    matches.push(match[1])
  }

  // console.log(`Possible classes (${matches.length}): ${matches.join(', ')}`)
  return {
    attributes: {
      names: [],
      values: [],
    },
    ids: [],
    tags,
    undetermined: [],
    classes: matches,
  }
}

const purgeCSSResult = await new PurgeCSS().purge({
  content: ['dist/assets/*.js'],
  css: ['dist/assets/*.css'],
  extractors: [
    {
      extractor: purgeFromJs,
      extensions: ['js'],
    },
  ],
})

// process.exit(0)

purgeCSSResult.forEach((result) => {
  const { css, file } = result
  if (!file) return
  const bytesBefore = Buffer.byteLength(readFileSync(file, 'utf-8'), 'utf-8')
  const bytesAfter = Buffer.byteLength(css, 'utf-8')
  console.log(`Writing optimized css: ${file}`)
  writeFileSync(file, css)
  console.log(`Reduced size from ${bytesBefore} to ${bytesAfter} bytes`)
})

benallfree avatar Jun 22 '25 10:06 benallfree

Hey! Great solution you've come up with, I'm just not sure if this should be done at compile level - unused css does more harm than ending up at the client. I'm speaking of maintainability, component pollution and such. This would be something that should probably be done during linting or so, or what do you think? :)

simonwep avatar Nov 03 '25 05:11 simonwep