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

Reduce bundle size via one-letter css classname hash strategy #1028

Open
denisx opened this issue Dec 19, 2019 · 2 comments
Open

Reduce bundle size via one-letter css classname hash strategy #1028

denisx opened this issue Dec 19, 2019 · 2 comments

Comments

@denisx
Copy link

denisx commented Dec 19, 2019

A worked code sample for https://dev.to/denisx/reduce-bundle-size-via-one-letter-css-classname-hash-strategy-10g6

Improving bundle compression to 40% of filesize via change standard css classname hash for splitting to one-letter name strategy and filepath.

webpack

// at header
const OneLetterCss = require('../utils/one-letter-css');
const MyOneLetterCss = new OneLetterCss();

// config
module: {
        rules: [
             {
                test: /\.css$/,
                use: [
                    {
                        loader: 'css-loader',
                        options: {
                            sourceMap: true,
                            modules: {
                                localIdentName: '[hash:base64:8]',
                                getLocalIdent: MyOneLetterCss.getLocalIdent
...

class

/**
 * @author denisx <github.com@denisx.com>
 */

const loaderUtils = require('loader-utils');

module.exports = class OneLetterCss {
  constructor() {
    // Save char symbol start positions
    this.a = 'a'.charCodeAt(0);
    this.A = 'A'.charCodeAt(0);
    // file hashes cache
    this.files = {};
    this.lastUsedFiles = -1;
    this.lastUsedClasses = -1;
    /** encoding [a-zA-Z] */
    this.symbols = 52;
    /** a half of encoding */
    this.half = 26;
    /** prevent loop-hell */
    this.maxLoop = 10;
  }

  /** encoding by rule count at file, 0 - a, 1 - b, 51 - Z, 52 - ba, etc */
  getName(lastUsed) {
    const { a, A, symbols, maxLoop, half } = this;
    let name = '';
    let loop = 0;
    let main = lastUsed;
    let tail = 0;

    while (
      ((main > 0 && tail >= 0) ||
        // first step anyway needed
        loop === 0) &&
      loop < maxLoop
    ) {
      const newMain = Math.floor(main / symbols);

      tail = main % symbols;
      name = String.fromCharCode((tail >= half ? A - half : a) + tail) + name;
      main = newMain;
      loop += 1;
    }

    return name;
  }

  getLocalIdent(context, localIdentName, localName) {
    const { resourcePath } = context;
    const { files } = this;

    // check file data at cache by absolute path
    let fileShort = files[resourcePath];

    // no file data, lets generate and save
    if (!fileShort) {
      this.lastUsedFiles += 1;

      // if we know file position, we must use base52 encoding with '_'
      // between rule position and file position
      // to avoid collapse hash combination. a_ab vs aa_b
      const fileShortName = loaderUtils.interpolateName(
        context,
        '[hash:base64:8]',
        {
          content: resourcePath,
        }
      );

      fileShort = { name: fileShortName, lastUsed: -1, ruleNames: {} };
      files[resourcePath] = fileShort;
    }

    // Get generative rule name from this file
    let newRuleName = fileShort.ruleNames[localName];

    // If no rule - renerate new, and save
    if (!newRuleName) {
      // Count +1
      fileShort.lastUsed += 1;

      // Generate new rule name
      newRuleName = this.getName(fileShort.lastUsed) + fileShort.name;

      // Saved
      fileShort.ruleNames[localName] = newRuleName;
    }

    // If has "local" at webpack settings
    const hasLocal = /\[local]/.test(localIdentName);

    // If has - add prefix
    return hasLocal ? `${localName}__${newRuleName}` : newRuleName;
  }
};

tests

import OneLetterCss from '../src/plugins/one-letter-css';

/* webpack set */
const workSets = [
  {
    in: [
      {
        resourcePath: './file1.css',
      },
      '[hash:base64:8]',
      'theme-white',
    ],
    out: ['a2zADNwsK'],
  },
  {
    in: [
      {
        resourcePath: './file1.css',
      },
      '[hash:base64:8]',
      'theme-blue',
    ],
    out: ['b2zADNwsK'],
  },
  {
    in: [
      {
        resourcePath: './file2.css',
      },
      '[hash:base64:8]',
      'text-white',
    ],
    out: ['a2jlx459O'],
  },
  {
    in: [
      {
        resourcePath: './file2.css',
      },
      '[hash:base64:8]',
      'text-blue',
    ],
    out: ['b2jlx459O'],
  },
  // for develop case
  {
    in: [
      {
        resourcePath: './file2.css',
      },
      '[local]__[hash:base64:8]',
      'text-blue',
    ],
    out: ['text-blue__b2jlx459O'],
  },
];

/* encoding test set */
const encodingSets = [
  {
    in: [0],
    out: ['a'],
  },
  {
    in: [1],
    out: ['b'],
  },
  {
    in: [51],
    out: ['Z'],
  },
  {
    in: [52],
    out: ['ba'],
  },
  {
    in: [53],
    out: ['bb'],
  },
];

const MyOneLetterCss = new OneLetterCss();

describe('testing work case', () => {
  workSets.forEach((set) => {
    it(`should check name generate`, () => {
      expect(MyOneLetterCss.getLocalIdent(...set.in)).toEqual(...set.out);
    });
  });
});

describe('testing encoding method', () => {
  encodingSets.forEach((set) => {
    it(`should check name generate`, () => {
      expect(MyOneLetterCss.getName(...set.in)).toEqual(...set.out);
    });
  });
});

p.s. need help with more magic - how to keep file tree sorted between client and server variants. this will reduce 81% of filesize.

@sokra
Copy link
Member

sokra commented Dec 20, 2019

need help with more magic - how to keep file tree sorted between client and server variants.

I would say one way would be to keep a JSON file with all CSS files in your src folder and take the index. For any CSS file found during build that is not in the list emit a warning (or error) and use a hash instead.

Even while this is against the guideline of webpack build should not write to intput files, you could offer a flag to automatically add new files to the JSON file during development. I would recommend to use an error for missing files during production.

@denisx
Copy link
Author

denisx commented Dec 20, 2019

I would say one way would be to keep a JSON file with all CSS files in your src folder and take the index. For any CSS file found during build that is not in the list emit a warning (or error) and use a hash instead.

@sokra I thinks about access to file tree (for sort and count) before module parsing...

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

No branches or pull requests

3 participants