@divriots/jampack 0.20.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -106,7 +106,21 @@ Lazy-load assets below-the-fold ⬇️.
106
106
 
107
107
  - Images and Iframes are lazy loaded.
108
108
 
109
- See [details](https://jampack.divriots.com/features/optimize-above-the-fold/).
109
+ See [Above-the-fold optimisations](https://jampack.divriots.com/features/optimize-above-the-fold/) for details.
110
+
111
+ ## Inline critical CSS
112
+
113
+ Avoid [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) while stylesheets are downloaded and parsed.
114
+ `jampack` will inline critical CSS and lazy-load the rest of CSS.
115
+
116
+ See [Inline critical CSS](https://jampack.divriots.com/features/inline-critical-css/) for details.
117
+
118
+ ## Prefetch links
119
+
120
+ Speedup the future page navigations by prefetching the links on the page.
121
+ Thanks to [quicklink](https://github.com/GoogleChromeLabs/quicklink), this can be done dynamically as links enter the viewport.
122
+
123
+ See [Prefetch links](https://jampack.divriots.com/features/prefetch-links/) for details.
110
124
 
111
125
  ### All assets are compressed
112
126
 
package/dist/cache.js CHANGED
@@ -4,7 +4,7 @@ import * as fs from 'fs/promises';
4
4
  import $state from './state.js';
5
5
  import { CACHE_VERSIONS } from './packagejson.js';
6
6
  const listOfCategories = ['img', 'img-ext'];
7
- const CACHE_FOLDER = '.jampack/cache';
7
+ const CACHE_FOLDER = $state.args.cache_folder || '.jampack/cache';
8
8
  async function cleanCache(full) {
9
9
  if (full) {
10
10
  try {
@@ -3,7 +3,7 @@ const default_options = {
3
3
  add_css_reset_as: 'inline',
4
4
  },
5
5
  css: {
6
- inline_critical_css: false, // Doesn't work yet
6
+ inline_critical_css: false,
7
7
  },
8
8
  js: {
9
9
  compressor: 'esbuild',
package/dist/index.js CHANGED
@@ -38,11 +38,14 @@ program
38
38
  .option('--fail', 'Exits with a non-zero return code if issues.')
39
39
  .option('--onlyoptim', 'Only optimize (PASS 1).')
40
40
  .option('--onlycomp', 'Only compress (PASS 2).')
41
+ .option('--cache_folder <cache_folder>', 'Default: .jampack/cache')
41
42
  .option('--cleancache', 'Clean cache before running')
42
43
  .option('--nocache', 'Run with no use of cache')
43
44
  .action(async (dir, options) => {
45
+ // Arguments
44
46
  $state.dir = dir;
45
47
  $state.args = options;
48
+ // Print options
46
49
  if (options) {
47
50
  console.log('Options:');
48
51
  console.log(options);
package/dist/optimize.js CHANGED
@@ -121,8 +121,7 @@ async function analyse(file) {
121
121
  //
122
122
  if (config.css.inline_critical_css) {
123
123
  try {
124
- const location = path.resolve(path.dirname(path.join($state.dir, file)));
125
- htmlResult = await inlineCriticalCss(location, htmlResult);
124
+ htmlResult = await inlineCriticalCss($state.dir, htmlResult);
126
125
  }
127
126
  catch (e) {
128
127
  console.warn('Fail to inline critical CSS', e);
@@ -156,7 +155,7 @@ function getTheFold($) {
156
155
  return 0;
157
156
  }
158
157
  async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
159
- var _a, _b;
158
+ var _a, _b, _c;
160
159
  const img = $(imgElement);
161
160
  /*
162
161
  * Attribute 'src'
@@ -265,19 +264,17 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
265
264
  let srcToFormat = 'unchanged';
266
265
  if (isAboveTheFold) {
267
266
  // Above the fold, nothing beats progressive JPEG for LCP
268
- // but it's not transparent
269
- if ((_b = (await originalImage.getImageMeta())) === null || _b === void 0 ? void 0 : _b.hasAlpha) {
270
- // TODO some images have Alpha metadata but have 0 alpha pixels
271
- // We should be able to detect these to take advantage of Progressive JPEG
267
+ // But jpeg doesn't support transparency
268
+ if ((_b = (await originalImage.getImageMeta())) === null || _b === void 0 ? void 0 : _b.isOpaque) {
269
+ srcToFormat = 'jpeg+progressive';
270
+ }
271
+ else {
272
272
  srcToFormat =
273
273
  (await originalImage.getMime()) === 'image/avif' ||
274
274
  (await originalImage.getMime()) === 'image/webp'
275
275
  ? 'unchanged'
276
276
  : 'webp';
277
277
  }
278
- else {
279
- srcToFormat = 'jpeg+progressive';
280
- }
281
278
  }
282
279
  else if (isInPicture) {
283
280
  // srcToFormat should not change if in picture
@@ -429,18 +426,29 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
429
426
  // Only try to generate better images
430
427
  // TODO generate more compatible formats (avif => webp/jpeg/png)
431
428
  //
432
- switch (await originalImage.getMime()) {
433
- case 'image/jpeg':
429
+ const mime = await originalImage.getMime();
430
+ switch (mime) {
434
431
  case 'image/png':
432
+ case 'image/jpeg':
435
433
  sourcesToGenerate.push({
436
434
  mime: 'image/webp',
437
435
  format: 'webp',
438
436
  });
439
437
  case 'image/webp':
440
- sourcesToGenerate.push({
441
- mime: 'image/avif',
442
- format: 'avif',
443
- });
438
+ // Only add AVIF source for lossy images
439
+ // AVIF don't perform well on lossless images
440
+ // PNG and WebP will perform better
441
+ // https://www.reddit.com/r/jpegxl/comments/l9ta2u/how_does_lossless_jpegxl_compared_to_png/
442
+ // https://twitter.com/jonsneyers/status/1346389917816008704?s=19
443
+ const isLossless = mime === 'image/png' ||
444
+ (mime === 'image/webp' &&
445
+ ((_c = (await originalImage.getImageMeta())) === null || _c === void 0 ? void 0 : _c.isLossless));
446
+ if (!isLossless) {
447
+ sourcesToGenerate.push({
448
+ mime: 'image/avif',
449
+ format: 'avif',
450
+ });
451
+ }
444
452
  case 'image/avif':
445
453
  // Nothing better
446
454
  break;
@@ -4,6 +4,8 @@ export function inlineCriticalCss(path, html) {
4
4
  const critters = new Critters({
5
5
  compress: false,
6
6
  fonts: false,
7
+ reduceInlineStyles: false,
8
+ inlineThreshold: 0,
7
9
  logLevel: 'info',
8
10
  path,
9
11
  });
package/dist/state.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type Args = {
2
2
  nowrite?: boolean;
3
3
  nocache?: boolean;
4
+ cache_folder?: string;
4
5
  };
5
6
  export type ReportItem = {
6
7
  action: string;
@@ -5,7 +5,8 @@ type ImageMeta = {
5
5
  width: number | undefined;
6
6
  height: number | undefined;
7
7
  isProgressive: boolean;
8
- hasAlpha: boolean;
8
+ isOpaque: boolean;
9
+ isLossless: boolean;
9
10
  };
10
11
  /**
11
12
  * File extension, mime and data are loaded only on demand, then cached.
@@ -21,21 +21,37 @@ export class Resource {
21
21
  async getImageMeta() {
22
22
  if (this.image_meta === undefined) {
23
23
  const ext = await this.getExt();
24
+ let isLossless = false;
24
25
  switch (ext) {
25
26
  case 'svg':
26
27
  case 'gif':
27
- case 'jpg':
28
28
  case 'png':
29
- case 'webp':
30
29
  case 'tif':
30
+ isLossless = true;
31
+ case 'webp':
32
+ if (!isLossless) {
33
+ try {
34
+ // TODO check if webp is lossless
35
+ // const info = await WebPInfo.from(await this.getData());
36
+ // isLossless = info.summary.isLossless;
37
+ }
38
+ catch (e) {
39
+ console.warn('Failed to get WebP info');
40
+ }
41
+ }
31
42
  case 'avif':
43
+ // TODO
44
+ // Check for lossless avif
45
+ case 'jpg':
32
46
  let sharpFile = await sharp(await this.getData(), { animated: true });
33
47
  const meta = await sharpFile.metadata();
48
+ const stats = await sharpFile.stats();
34
49
  this.image_meta = {
35
50
  width: meta.width,
36
51
  height: meta.height,
37
52
  isProgressive: meta.isProgressive || false,
38
- hasAlpha: meta.hasAlpha || false,
53
+ isOpaque: stats.isOpaque,
54
+ isLossless,
39
55
  };
40
56
  break;
41
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divriots/jampack",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "cache-version": {
5
5
  "img": "v1",
6
6
  "img-ext": "v1"
@@ -22,29 +22,29 @@
22
22
  "dependencies": {
23
23
  "@divriots/cheerio": "1.0.0-rc.12",
24
24
  "@proload/core": "^0.3.3",
25
- "@swc/core": "^1.3.82",
25
+ "@swc/core": "^1.3.86",
26
26
  "add": "^2.0.6",
27
27
  "commander": "^10.0.1",
28
28
  "critters": "^0.0.20",
29
29
  "deepmerge": "^4.3.1",
30
- "esbuild": "^0.19.2",
30
+ "esbuild": "^0.19.3",
31
31
  "file-type": "^18.5.0",
32
32
  "globby": "^13.2.2",
33
33
  "hasha": "^5.2.2",
34
34
  "html-minifier-terser": "^7.2.0",
35
35
  "kleur": "^4.1.5",
36
- "lightningcss": "^1.21.5",
36
+ "lightningcss": "^1.22.0",
37
37
  "mini-svg-data-uri": "^1.4.4",
38
38
  "ora": "^7.0.1",
39
39
  "quicklink": "^2.3.0",
40
- "sharp": "^0.32.5",
40
+ "sharp": "^0.32.6",
41
41
  "svgo": "^3.0.2",
42
42
  "table": "^6.8.1",
43
- "undici": "^5.23.0"
43
+ "undici": "^5.24.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/html-minifier-terser": "^7.0.0",
47
- "@types/node": "^20.5.7",
47
+ "@types/node": "^20.6.2",
48
48
  "shx": "^0.3.3",
49
49
  "ts-node": "^10.2.1",
50
50
  "tslib": "^2.6.2",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "scripts": {
54
54
  "watch": "pnpm build --watch",
55
- "try": "shx rm -fr demo && cp -R demo_ref demo && node ./dist/index.js ./demo --nocache",
55
+ "try": "shx rm -fr demo && cp -R demo_drc demo && node ./dist/index.js ./demo --nocache",
56
56
  "build": "shx rm -rf dist && tsc -b",
57
57
  "lint": "eslint . --ext .ts --config .eslintrc",
58
58
  "prepublishOnly": "pnpm build",