monokee / Sekoia

Sekoia.js - Reactivity Engine
Other
33 stars 0 forks source link

Compile Time Optimizations #14

Open monokee opened 4 years ago

monokee commented 4 years ago

CSS Engine

Consider compile time parsing -> bundling, auto-prefixing and minification. Remove the CSS template strings from cue javascript components entirely and write them into a distributable CSS package which can be autoprefixed and minified, further reducing initial page load times due to reduced bundle size and less cpu cycles spent on parsing/scoping/appending the CSS on a per-component level.

Should this be written as a gulp plugin or as a rollup plugin?

monokee commented 2 years ago

I don't think this idea should be abandoned. It would be relatively easy to pre-compile both html templates and component css with JSDOM.

monokee commented 2 years ago

By extracting the entire component css Stylesheet we're sacrificing the lazy loading aspect of components and are optimistically adding the entire css at page load.

Maybe we should not attach ALL components virtually at build time but instead focus on what a build step is actually supposed to optimize: initial page load. So we just pre-render the components which are active after initial page load without asynchronous interaction by an external agent. We're essentially pre-rendering an application shell while keeping everything else lazy and dynamic. I like that.

monokee commented 2 years ago

Priority for this should be pretty low because the lazy component instantiation mechanism in sekoia already makes our 1000+ components apps super responsive.

monokee commented 1 year ago

I've since written a Gulp task that extracts the style property from components and adds it into a separate style sheet:


function extractCSS(stream) {

  const DEFINE_COMPONENT_REGEX = new RegExp('(defineComponent|createComponent|\\.extend)\\(\'(.|[\\n\\r])+?}\\)', 'g');
  const COMPONENT_NAME_REGEX = new RegExp('(defineComponent|createComponent|\\.extend)\\(\'([\\w-]+?)\',\\s?{');
  const COMPONENT_STYLE_REGEX = new RegExp('style:\\s?\\(`((.|[\\n\\r])+?)\`\\),?');
  const STYLE_URL_REGEX = /url\((?!['"]?:)['"]?([^'")]*)['"]?\)/ // url is [1]

  return stream.pipe(new Transform({

      objectMode: true,

      transform(file, enc, callback) {

        if (!file.isNull() && file.isBuffer()) {

          try {

            let content = String(file.contents);
            let componentStyles = '';
            const componentDefinitions = content.match(DEFINE_COMPONENT_REGEX);

            componentDefinitions.forEach(component => {

              const styleGroups = component.match(COMPONENT_STYLE_REGEX);

              if (styleGroups) {

                // remove style from js components
                if (styleGroups[0]) { // entire style definition style: (`:host {...}`)
                  content = content.replace(styleGroups[0], '');
                }

                // write transformed styles to string that will be put into a css file next
                if (styleGroups[1]) { // inside css styles only: :host {...}

                  const name = component.match(COMPONENT_NAME_REGEX)[2];

                  // replace :host with component-name and extension
                  let transformedStyle = styleGroups[1].replaceAll(':host', `:is(${name}, [extends="${name}"])`);

                  // check if style contains urls
                  const url = transformedStyle.match(STYLE_URL_REGEX);

                  if (url && url[1]) {
                    if (url[1].startsWith('/')) {
                      console.warn('Did not transform Component CSS URL starting with hyphen: ' + url[1]);
                    } else if (!url[1].startsWith('data:') && !url[1].startsWith('http')) {
                      // add ../ to relative urls
                      transformedStyle = transformedStyle.replace(url[1], `../${url[1]}`);
                    }
                  }

                  componentStyles += transformedStyle;

                }

              }

            });

            file.contents = Buffer.from(content);

            if (componentStyles.length) {

              const { dir } = getNameAndDir(config.extractComponentCSS.index);
              const { name } = getNameAndDir(config.extractComponentCSS.target);

              src(config.extractComponentCSS.index).pipe(gap.appendText(`
                /* --- Extracted from defineComponent --- */
                ${componentStyles}
              `)).pipe(rename(name)).pipe(dest(dir)).pipe(new Transform({
                objectMode: true,
                transform(file2, enc2, callback2) {
                  // only doing this because I couldn't figure out how to execute callback1 after dest()
                  callback(null, file);
                  callback2(null, file2);
                }
              }));

            } else {

              callback(null, file);

            }

          } catch (e) {
            throw new Error('CSS EXTRACT ERROR ' + e.message);
          }

        } else {

          callback(null, file);

        }

      }
    }));
}