Skip to content

Commit

Permalink
perf: cache processed stylesheets
Browse files Browse the repository at this point in the history
With this change we cache processed component CSS so that during the 2nd cold build we only compile changed CSS.
  • Loading branch information
alan-agius4 committed May 6, 2021
1 parent ff90621 commit b791429
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 16 deletions.
4 changes: 4 additions & 0 deletions package.json
Expand Up @@ -35,9 +35,11 @@
"ajv": "^8.0.0",
"ansi-colors": "^4.1.1",
"browserslist": "^4.16.1",
"cacache": "^15.0.6",
"chokidar": "^3.5.1",
"commander": "^7.0.0",
"cssnano": "^5.0.0",
"find-cache-dir": "^3.3.1",
"glob": "^7.1.6",
"injection-js": "^2.4.0",
"jsonc-parser": "^3.0.0",
Expand Down Expand Up @@ -71,9 +73,11 @@
"@commitlint/cli": "^12.0.0",
"@commitlint/config-angular": "^12.0.0",
"@types/acorn": "^4.0.3",
"@types/cacache": "^15.0.0",
"@types/chai": "^4.0.0",
"@types/chokidar": "^1.7.5",
"@types/cssnano": "^4.0.0",
"@types/find-cache-dir": "^3.2.0",
"@types/fs-extra": "^9.0.10",
"@types/glob": "^7.0.0",
"@types/jasmine": "^3.0.0",
Expand Down
41 changes: 33 additions & 8 deletions src/lib/styles/stylesheet-processor-worker.ts
@@ -1,3 +1,5 @@
import * as cacache from 'cacache';
import { createHash } from 'crypto';
import * as path from 'path';
import postcss, { LazyResult } from 'postcss';
import * as postcssUrl from 'postcss-url';
Expand All @@ -8,28 +10,51 @@ import * as postcssPresetEnv from 'postcss-preset-env';
import { CssUrl, WorkerOptions, WorkerResult } from './stylesheet-processor';
import { readFile } from '../utils/fs';

const ngPackagrVersion = require('../../../package.json').version;

async function processCss({
filePath,
browserslistData,
cssUrl,
styleIncludePaths,
basePath,
cachePath,
}: WorkerOptions): Promise<WorkerResult> {
const content = await readFile(filePath, 'utf8');

// Render pre-processor language (sass, styl, less)
const renderedCss = await renderCss(filePath, basePath, styleIncludePaths);
const renderedCss = await renderCss(filePath, content, basePath, styleIncludePaths);

// Render postcss (autoprefixing and friends)
const result = await optimizeCss(filePath, renderedCss, browserslistData, cssUrl);

// We cannot cache CSS re-rendering phase, because a transitive dependency via (@import) can case different CSS output.
// Example a change in a mixin or SCSS variable.
const key = createHash('sha1')
.update(ngPackagrVersion)
.update(result.css)
.update(browserslistData.join(''))
.digest('base64');

const entry = await cacache.get.info(cachePath, key);
if (entry) {
return {
css: await readFile(entry.path, 'utf8'),
warnings: [],
};
}

// Add to cache
await cacache.put(cachePath, key, result.css);

return {
css: result.css,
warnings: result.warnings().map(w => w.toString()),
};
}

async function renderCss(filePath: string, basePath, styleIncludePaths?: string[]): Promise<string> {
async function renderCss(filePath: string, css: string, basePath, styleIncludePaths?: string[]): Promise<string> {
const ext = path.extname(filePath);
const content = await readFile(filePath, 'utf8');

switch (ext) {
case '.sass':
Expand All @@ -53,28 +78,28 @@ async function renderCss(filePath: string, basePath, styleIncludePaths?: string[
return sassCompiler
.renderSync({
file: filePath,
data: content,
data: css,
indentedSyntax: '.sass' === ext,
importer: await import('node-sass-tilde-importer'),
includePaths: styleIncludePaths,
})
.css.toString();
}
case '.less': {
const { css } = await (await import('less')).render(content, {
const { css: content } = await (await import('less')).render(css, {
filename: filePath,
javascriptEnabled: true,
paths: styleIncludePaths,
});

return css;
return content;
}
case '.styl':
case '.stylus': {
const stylus = await import('stylus');

return (
stylus(content)
stylus(css)
// add paths for resolve
.set('paths', [basePath, '.', ...styleIncludePaths, 'node_modules'])
// add support for resolving plugins from node_modules
Expand All @@ -87,7 +112,7 @@ async function renderCss(filePath: string, basePath, styleIncludePaths?: string[
}
case '.css':
default:
return content;
return css;
}
}

Expand Down
17 changes: 13 additions & 4 deletions src/lib/styles/stylesheet-processor.ts
@@ -1,5 +1,7 @@
import * as browserslist from 'browserslist';
import { join } from 'path';
import * as findCacheDirectory from 'find-cache-dir';
import { tmpdir } from 'os';
import { MessageChannel, receiveMessageOnPort, Worker } from 'worker_threads';

import * as log from '../utils/log';
Expand All @@ -14,6 +16,7 @@ export interface WorkerOptions {
browserslistData: string[];
cssUrl?: CssUrl;
styleIncludePaths?: string[];
cachePath: string;
}

export interface WorkerResult {
Expand All @@ -25,8 +28,15 @@ export interface WorkerResult {
export class StylesheetProcessor {
private browserslistData: string[] | undefined;
private worker: Worker | undefined;
private readonly cachePath: string;

constructor(private readonly basePath: string, private readonly cssUrl?: CssUrl, private readonly styleIncludePaths?: string[]) { }
constructor(
private readonly basePath: string,
private readonly cssUrl?: CssUrl,
private readonly styleIncludePaths?: string[],
) {
this.cachePath = findCacheDirectory({ name: 'ng-packagr-styles' }) || tmpdir();
}

process(filePath: string) {
if (!this.worker) {
Expand All @@ -44,15 +54,14 @@ export class StylesheetProcessor {
cssUrl: this.cssUrl,
styleIncludePaths: this.styleIncludePaths,
browserslistData: this.browserslistData,
cachePath: this.cachePath,
};

const ioChannel = new MessageChannel();

try {
const signal = new Int32Array(new SharedArrayBuffer(4));
this.worker.postMessage({ signal, port: ioChannel.port1, workerOptions }, [
ioChannel.port1
]);
this.worker.postMessage({ signal, port: ioChannel.port1, workerOptions }, [ioChannel.port1]);

// Sleep until signal[0] is 0
Atomics.wait(signal, 0, 0);
Expand Down

0 comments on commit b791429

Please sign in to comment.