@turntrout/subfont 1.5.1 → 1.7.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/CHANGELOG.md +7 -0
- package/README.md +52 -21
- package/lib/FontTracerPool.js +49 -1
- package/lib/HeadlessBrowser.js +11 -3
- package/lib/collectTextsByPage.js +496 -651
- package/lib/concurrencyLimit.js +3 -1
- package/lib/escapeJsStringLiteral.js +13 -0
- package/lib/extractVisibleText.js +6 -2
- package/lib/fontConverter.js +25 -0
- package/lib/fontConverterWorker.js +16 -0
- package/lib/fontFaceHelpers.js +16 -4
- package/lib/fontFeatureHelpers.js +249 -0
- package/lib/fontTracerWorker.js +0 -10
- package/lib/gatherStylesheetsWithPredicates.js +4 -5
- package/lib/normalizeFontPropertyValue.js +1 -1
- package/lib/progress.js +101 -0
- package/lib/sfntCache.js +10 -7
- package/lib/subfont.js +80 -52
- package/lib/subsetFontWithGlyphs.js +41 -22
- package/lib/subsetFonts.js +223 -211
- package/lib/subsetGeneration.js +13 -1
- package/lib/unquote.js +9 -4
- package/lib/variationAxes.js +3 -32
- package/lib/warnAboutMissingGlyphs.js +36 -25
- package/lib/wasmQueue.js +6 -2
- package/package.json +2 -2
package/lib/subsetFonts.js
CHANGED
|
@@ -7,6 +7,7 @@ const compileQuery = require('assetgraph/lib/compileQuery');
|
|
|
7
7
|
const findCustomPropertyDefinitions = require('./findCustomPropertyDefinitions');
|
|
8
8
|
const extractReferencedCustomPropertyNames = require('./extractReferencedCustomPropertyNames');
|
|
9
9
|
const injectSubsetDefinitions = require('./injectSubsetDefinitions');
|
|
10
|
+
const { makePhaseTracker } = require('./progress');
|
|
10
11
|
const cssFontParser = require('css-font-parser');
|
|
11
12
|
const cssListHelpers = require('css-list-helpers');
|
|
12
13
|
const unquote = require('./unquote');
|
|
@@ -15,6 +16,7 @@ const unicodeRange = require('./unicodeRange');
|
|
|
15
16
|
const getFontInfo = require('./getFontInfo');
|
|
16
17
|
const collectTextsByPage = require('./collectTextsByPage');
|
|
17
18
|
|
|
19
|
+
const escapeJsStringLiteral = require('./escapeJsStringLiteral');
|
|
18
20
|
const {
|
|
19
21
|
maybeCssQuote,
|
|
20
22
|
getFontFaceDeclarationText,
|
|
@@ -54,16 +56,14 @@ function getParents(asset, assetQuery) {
|
|
|
54
56
|
return parents;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.replace(/`/g, '\\x60')
|
|
66
|
-
.replace(/</g, '\\x3c');
|
|
59
|
+
function countUniqueFontUrls(htmlOrSvgAssetTextsWithProps) {
|
|
60
|
+
const urls = new Set();
|
|
61
|
+
for (const item of htmlOrSvgAssetTextsWithProps) {
|
|
62
|
+
for (const fu of item.fontUsages) {
|
|
63
|
+
if (fu.fontUrl) urls.add(fu.fontUrl);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return urls.size;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function asyncLoadStyleRelationWithFallback(
|
|
@@ -214,8 +214,8 @@ const validFontDisplayValues = [
|
|
|
214
214
|
|
|
215
215
|
const warnAboutMissingGlyphs = require('./warnAboutMissingGlyphs');
|
|
216
216
|
|
|
217
|
-
//
|
|
218
|
-
//
|
|
217
|
+
// Create (or retrieve from disk cache) the subset CSS asset for a set of
|
|
218
|
+
// fontUsages, relocating the font binary to its hashed URL under subsetUrl.
|
|
219
219
|
async function getOrCreateSubsetCssAsset({
|
|
220
220
|
assetGraph,
|
|
221
221
|
subsetCssText,
|
|
@@ -311,7 +311,8 @@ async function getOrCreateSubsetCssAsset({
|
|
|
311
311
|
return cssAsset;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
//
|
|
314
|
+
// Insert <link rel="preload"> hints for every woff2 subset flagged as
|
|
315
|
+
// preload-worthy, so the browser starts fetching them during HTML parse.
|
|
315
316
|
function addSubsetFontPreloads({
|
|
316
317
|
cssAsset,
|
|
317
318
|
fontUsages,
|
|
@@ -362,6 +363,104 @@ function addSubsetFontPreloads({
|
|
|
362
363
|
return insertionPoint;
|
|
363
364
|
}
|
|
364
365
|
|
|
366
|
+
// Skip Google Fonts populate when no Google Fonts references exist —
|
|
367
|
+
// otherwise assetgraph spends ~30s network-walking for nothing on sites
|
|
368
|
+
// that only self-host. Returns whether the populate ran so callers can
|
|
369
|
+
// annotate their phase timing.
|
|
370
|
+
async function populateGoogleFontsIfPresent(assetGraph) {
|
|
371
|
+
const hasGoogleFonts =
|
|
372
|
+
assetGraph.findRelations({
|
|
373
|
+
to: { url: { $regex: googleFontsCssUrlRegex } },
|
|
374
|
+
}).length > 0;
|
|
375
|
+
if (!hasGoogleFonts) return false;
|
|
376
|
+
|
|
377
|
+
await assetGraph.populate({
|
|
378
|
+
followRelations: {
|
|
379
|
+
$or: [
|
|
380
|
+
{ to: { url: { $regex: googleFontsCssUrlRegex } } },
|
|
381
|
+
{
|
|
382
|
+
type: 'CssFontFaceSrc',
|
|
383
|
+
from: { url: { $regex: googleFontsCssUrlRegex } },
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Strip every original @font-face rule when --no-fallbacks is set. The
|
|
392
|
+
// severed assets are returned via the `potentiallyOrphanedAssets` set so
|
|
393
|
+
// the final orphan sweep can remove anything left dangling.
|
|
394
|
+
function removeOriginalFontFaceRules(
|
|
395
|
+
htmlOrSvgAssets,
|
|
396
|
+
fontFaceDeclarationsByHtmlOrSvgAsset,
|
|
397
|
+
potentiallyOrphanedAssets
|
|
398
|
+
) {
|
|
399
|
+
for (const htmlOrSvgAsset of htmlOrSvgAssets) {
|
|
400
|
+
const accumulatedFontFaceDeclarations =
|
|
401
|
+
fontFaceDeclarationsByHtmlOrSvgAsset.get(htmlOrSvgAsset);
|
|
402
|
+
for (const { relations } of accumulatedFontFaceDeclarations) {
|
|
403
|
+
for (const relation of relations) {
|
|
404
|
+
potentiallyOrphanedAssets.add(relation.to);
|
|
405
|
+
if (relation.node.parentNode) {
|
|
406
|
+
relation.node.parentNode.removeChild(relation.node);
|
|
407
|
+
}
|
|
408
|
+
relation.remove();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
htmlOrSvgAsset.markDirty();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Rewrite CSS source-map relations to the caller's chosen hrefType so they
|
|
416
|
+
// align with the rest of the emitted assets. Only invoked when sourceMaps is
|
|
417
|
+
// enabled — subsetFonts normally skips source-map serialization for speed.
|
|
418
|
+
async function rewriteCssSourceMaps(assetGraph, hrefType) {
|
|
419
|
+
await assetGraph.serializeSourceMaps(undefined, {
|
|
420
|
+
type: 'Css',
|
|
421
|
+
outgoingRelations: {
|
|
422
|
+
$where: (relations) =>
|
|
423
|
+
relations.some((relation) => relation.type === 'CssSourceMappingUrl'),
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
for (const relation of assetGraph.findRelations({
|
|
427
|
+
type: 'SourceMapSource',
|
|
428
|
+
})) {
|
|
429
|
+
relation.hrefType = hrefType;
|
|
430
|
+
}
|
|
431
|
+
for (const relation of assetGraph.findRelations({
|
|
432
|
+
type: 'CssSourceMappingUrl',
|
|
433
|
+
hrefType: { $in: ['relative', 'inline'] },
|
|
434
|
+
})) {
|
|
435
|
+
relation.hrefType = hrefType;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Remove assets whose last incoming relation was severed during subset
|
|
440
|
+
// injection (original @font-face rules, merged Google Fonts CSS, etc.) so
|
|
441
|
+
// the emitted site doesn't ship with dangling files.
|
|
442
|
+
function removeOrphanedAssets(assetGraph, potentiallyOrphanedAssets) {
|
|
443
|
+
for (const asset of potentiallyOrphanedAssets) {
|
|
444
|
+
if (asset.incomingRelations.length === 0) {
|
|
445
|
+
assetGraph.removeAsset(asset);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Shape the per-page fontUsages into the external fontInfo report: strip
|
|
451
|
+
// internal bookkeeping (subsets buffer, feature-tag scratch) and flatten
|
|
452
|
+
// each page to { assetFileName, fontUsages }.
|
|
453
|
+
function buildFontInfoReport(htmlOrSvgAssetTextsWithProps) {
|
|
454
|
+
return htmlOrSvgAssetTextsWithProps.map(({ fontUsages, htmlOrSvgAsset }) => ({
|
|
455
|
+
assetFileName: htmlOrSvgAsset.nonInlineAncestor.urlOrDescription,
|
|
456
|
+
fontUsages: fontUsages.map((fontUsage) =>
|
|
457
|
+
(({ subsets, hasFontFeatureSettings, fontFeatureTags, ...rest }) => rest)(
|
|
458
|
+
fontUsage
|
|
459
|
+
)
|
|
460
|
+
),
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
|
|
365
464
|
async function subsetFonts(
|
|
366
465
|
assetGraph,
|
|
367
466
|
{
|
|
@@ -396,50 +495,19 @@ async function subsetFonts(
|
|
|
396
495
|
const subsetUrl = urltools.ensureTrailingSlash(assetGraph.root + subsetPath);
|
|
397
496
|
|
|
398
497
|
const timings = {};
|
|
498
|
+
const trackPhase = makePhaseTracker(console, debug);
|
|
399
499
|
|
|
400
|
-
|
|
500
|
+
const applySourceMapsPhase = trackPhase('applySourceMaps');
|
|
401
501
|
if (sourceMaps) {
|
|
402
502
|
await assetGraph.applySourceMaps({ type: 'Css' });
|
|
403
503
|
}
|
|
404
|
-
timings.applySourceMaps =
|
|
405
|
-
if (debug && console)
|
|
406
|
-
console.log(
|
|
407
|
-
`[subfont timing] applySourceMaps: ${timings.applySourceMaps}ms`
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
phaseStart = Date.now();
|
|
411
|
-
// Only run Google Fonts populate if there are actually Google Fonts
|
|
412
|
-
// references in the graph. This avoids ~30s of wasted work on sites
|
|
413
|
-
// that use only self-hosted fonts.
|
|
414
|
-
const hasGoogleFonts =
|
|
415
|
-
assetGraph.findRelations({
|
|
416
|
-
to: { url: { $regex: googleFontsCssUrlRegex } },
|
|
417
|
-
}).length > 0;
|
|
504
|
+
timings.applySourceMaps = applySourceMapsPhase.end();
|
|
418
505
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
to: {
|
|
425
|
-
url: { $regex: googleFontsCssUrlRegex },
|
|
426
|
-
},
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
type: 'CssFontFaceSrc',
|
|
430
|
-
from: {
|
|
431
|
-
url: { $regex: googleFontsCssUrlRegex },
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
],
|
|
435
|
-
},
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
timings['populate (google fonts)'] = Date.now() - phaseStart;
|
|
439
|
-
if (debug && console)
|
|
440
|
-
console.log(
|
|
441
|
-
`[subfont timing] populate (google fonts): ${timings['populate (google fonts)']}ms${hasGoogleFonts ? '' : ' (skipped, no Google Fonts found)'}`
|
|
442
|
-
);
|
|
506
|
+
const googlePopulatePhase = trackPhase('populate (google fonts)');
|
|
507
|
+
const hasGoogleFonts = await populateGoogleFontsIfPresent(assetGraph);
|
|
508
|
+
timings['populate (google fonts)'] = googlePopulatePhase.end(
|
|
509
|
+
hasGoogleFonts ? null : 'skipped, no Google Fonts found'
|
|
510
|
+
);
|
|
443
511
|
|
|
444
512
|
const htmlOrSvgAssets = assetGraph.findAssets({
|
|
445
513
|
$or: [
|
|
@@ -453,11 +521,9 @@ async function subsetFonts(
|
|
|
453
521
|
],
|
|
454
522
|
});
|
|
455
523
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
);
|
|
460
|
-
const collectStart = Date.now();
|
|
524
|
+
const collectPhase = trackPhase(
|
|
525
|
+
`collectTextsByPage (${htmlOrSvgAssets.length} pages)`
|
|
526
|
+
);
|
|
461
527
|
const {
|
|
462
528
|
htmlOrSvgAssetTextsWithProps,
|
|
463
529
|
fontFaceDeclarationsByHtmlOrSvgAsset,
|
|
@@ -470,40 +536,21 @@ async function subsetFonts(
|
|
|
470
536
|
concurrency,
|
|
471
537
|
chromeArgs,
|
|
472
538
|
});
|
|
473
|
-
timings.collectTextsByPage =
|
|
539
|
+
timings.collectTextsByPage = collectPhase.end();
|
|
474
540
|
timings.collectTextsByPageDetails = subTimings;
|
|
475
|
-
if (debug && console)
|
|
476
|
-
console.log(
|
|
477
|
-
`[subfont timing] collectTextsByPage finished in ${timings.collectTextsByPage}ms`
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
phaseStart = Date.now();
|
|
481
541
|
|
|
542
|
+
const omitFallbacksPhase = trackPhase('omitFallbacks processing');
|
|
482
543
|
const potentiallyOrphanedAssets = new Set();
|
|
483
544
|
if (omitFallbacks) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
for (const relation of relations) {
|
|
490
|
-
potentiallyOrphanedAssets.add(relation.to);
|
|
491
|
-
if (relation.node.parentNode) {
|
|
492
|
-
relation.node.parentNode.removeChild(relation.node);
|
|
493
|
-
}
|
|
494
|
-
relation.remove();
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
htmlOrSvgAsset.markDirty();
|
|
498
|
-
}
|
|
545
|
+
removeOriginalFontFaceRules(
|
|
546
|
+
htmlOrSvgAssets,
|
|
547
|
+
fontFaceDeclarationsByHtmlOrSvgAsset,
|
|
548
|
+
potentiallyOrphanedAssets
|
|
549
|
+
);
|
|
499
550
|
}
|
|
551
|
+
timings['omitFallbacks processing'] = omitFallbacksPhase.end();
|
|
500
552
|
|
|
501
|
-
|
|
502
|
-
if (debug && console)
|
|
503
|
-
console.log(
|
|
504
|
-
`[subfont timing] omitFallbacks processing: ${timings['omitFallbacks processing']}ms`
|
|
505
|
-
);
|
|
506
|
-
phaseStart = Date.now();
|
|
553
|
+
const codepointPhase = trackPhase('codepoint generation');
|
|
507
554
|
|
|
508
555
|
if (fontDisplay) {
|
|
509
556
|
for (const htmlOrSvgAssetTextWithProps of htmlOrSvgAssetTextsWithProps) {
|
|
@@ -602,12 +649,7 @@ async function subsetFonts(
|
|
|
602
649
|
}
|
|
603
650
|
}
|
|
604
651
|
|
|
605
|
-
timings['codepoint generation'] =
|
|
606
|
-
if (debug && console)
|
|
607
|
-
console.log(
|
|
608
|
-
`[subfont timing] codepoint generation: ${timings['codepoint generation']}ms`
|
|
609
|
-
);
|
|
610
|
-
phaseStart = Date.now();
|
|
652
|
+
timings['codepoint generation'] = codepointPhase.end();
|
|
611
653
|
|
|
612
654
|
if (onlyInfo) {
|
|
613
655
|
return {
|
|
@@ -625,62 +667,61 @@ async function subsetFonts(
|
|
|
625
667
|
};
|
|
626
668
|
}
|
|
627
669
|
|
|
670
|
+
const variationPhase = trackPhase('variation axis usage');
|
|
628
671
|
const { seenAxisValuesByFontUrlAndAxisName } = getVariationAxisUsage(
|
|
629
672
|
htmlOrSvgAssetTextsWithProps,
|
|
630
673
|
parseFontWeightRange,
|
|
631
674
|
parseFontStretchRange
|
|
632
675
|
);
|
|
633
|
-
|
|
634
|
-
timings['variation axis usage'] = Date.now() - phaseStart;
|
|
635
|
-
if (debug && console)
|
|
636
|
-
console.log(
|
|
637
|
-
`[subfont timing] variation axis usage: ${timings['variation axis usage']}ms`
|
|
638
|
-
);
|
|
639
|
-
phaseStart = Date.now();
|
|
676
|
+
timings['variation axis usage'] = variationPhase.end();
|
|
640
677
|
|
|
641
678
|
// Generate subsets:
|
|
679
|
+
if (console) {
|
|
680
|
+
const uniqueFontUrls = countUniqueFontUrls(htmlOrSvgAssetTextsWithProps);
|
|
681
|
+
if (uniqueFontUrls > 0) {
|
|
682
|
+
console.log(
|
|
683
|
+
` Subsetting ${uniqueFontUrls} unique font file${uniqueFontUrls === 1 ? '' : 's'}...`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const subsetPhase = trackPhase('getSubsetsForFontUsage');
|
|
642
688
|
await getSubsetsForFontUsage(
|
|
643
689
|
assetGraph,
|
|
644
690
|
htmlOrSvgAssetTextsWithProps,
|
|
645
691
|
formats,
|
|
646
692
|
seenAxisValuesByFontUrlAndAxisName,
|
|
647
693
|
cacheDir,
|
|
648
|
-
console
|
|
694
|
+
console,
|
|
695
|
+
debug
|
|
649
696
|
);
|
|
697
|
+
timings.getSubsetsForFontUsage = subsetPhase.end();
|
|
650
698
|
|
|
651
|
-
|
|
652
|
-
if (debug && console)
|
|
653
|
-
console.log(
|
|
654
|
-
`[subfont timing] getSubsetsForFontUsage: ${timings.getSubsetsForFontUsage}ms`
|
|
655
|
-
);
|
|
656
|
-
phaseStart = Date.now();
|
|
657
|
-
|
|
699
|
+
const warnGlyphsPhase = trackPhase('warnAboutMissingGlyphs');
|
|
658
700
|
await warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph);
|
|
659
|
-
timings.warnAboutMissingGlyphs =
|
|
660
|
-
if (debug && console)
|
|
661
|
-
console.log(
|
|
662
|
-
`[subfont timing] warnAboutMissingGlyphs: ${timings.warnAboutMissingGlyphs}ms`
|
|
663
|
-
);
|
|
664
|
-
phaseStart = Date.now();
|
|
701
|
+
timings.warnAboutMissingGlyphs = warnGlyphsPhase.end();
|
|
665
702
|
|
|
666
703
|
// Insert subsets:
|
|
667
704
|
|
|
668
|
-
// Pre-compute which fontUrls are used (with text) on every page
|
|
669
|
-
//
|
|
705
|
+
// Pre-compute which fontUrls are used (with text) on every page.
|
|
706
|
+
// Set intersection: O(pages × fonts_per_page) vs the old every+some approach.
|
|
670
707
|
const fontUrlsUsedOnEveryPage = new Set();
|
|
671
708
|
if (htmlOrSvgAssetTextsWithProps.length > 0) {
|
|
672
|
-
// Start with all fontUrls from the first page
|
|
673
709
|
const firstPageFontUrls = new Set();
|
|
674
710
|
for (const fu of htmlOrSvgAssetTextsWithProps[0].fontUsages) {
|
|
675
711
|
if (fu.pageText) firstPageFontUrls.add(fu.fontUrl);
|
|
676
712
|
}
|
|
677
713
|
for (const fontUrl of firstPageFontUrls) {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
) {
|
|
683
|
-
|
|
714
|
+
fontUrlsUsedOnEveryPage.add(fontUrl);
|
|
715
|
+
}
|
|
716
|
+
for (let i = 1; i < htmlOrSvgAssetTextsWithProps.length; i++) {
|
|
717
|
+
const pageFontUrls = new Set();
|
|
718
|
+
for (const fu of htmlOrSvgAssetTextsWithProps[i].fontUsages) {
|
|
719
|
+
if (fu.pageText) pageFontUrls.add(fu.fontUrl);
|
|
720
|
+
}
|
|
721
|
+
for (const fontUrl of fontUrlsUsedOnEveryPage) {
|
|
722
|
+
if (!pageFontUrls.has(fontUrl)) {
|
|
723
|
+
fontUrlsUsedOnEveryPage.delete(fontUrl);
|
|
724
|
+
}
|
|
684
725
|
}
|
|
685
726
|
}
|
|
686
727
|
}
|
|
@@ -689,6 +730,12 @@ async function subsetFonts(
|
|
|
689
730
|
// addAsset/minify/removeAsset cycles for pages sharing identical CSS.
|
|
690
731
|
const subsetCssAssetCache = new Map();
|
|
691
732
|
|
|
733
|
+
// Cache the heavy CSS-text assembly (including base64-encoded font data)
|
|
734
|
+
// keyed by the shared accumulatedFontFaceDeclarations array. Pages grouped
|
|
735
|
+
// under the same stylesheet config produce byte-identical output, so this
|
|
736
|
+
// collapses the per-page string build from O(pages) to O(unique configs).
|
|
737
|
+
const subsetCssTextCache = new WeakMap();
|
|
738
|
+
|
|
692
739
|
// Pre-index relations by source asset to avoid O(allRelations) scans
|
|
693
740
|
// in the per-page injection loop below. Build indices once, then use
|
|
694
741
|
// O(1) lookups per page instead of repeated assetGraph.findRelations.
|
|
@@ -711,6 +758,9 @@ async function subsetFonts(
|
|
|
711
758
|
index.get(from).push(relation);
|
|
712
759
|
}
|
|
713
760
|
|
|
761
|
+
const insertPhase = trackPhase(
|
|
762
|
+
`insert subsets loop (${htmlOrSvgAssetTextsWithProps.length} pages)`
|
|
763
|
+
);
|
|
714
764
|
let numFontUsagesWithSubset = 0;
|
|
715
765
|
for (const {
|
|
716
766
|
htmlOrSvgAsset,
|
|
@@ -783,11 +833,19 @@ async function subsetFonts(
|
|
|
783
833
|
}
|
|
784
834
|
numFontUsagesWithSubset += subsetFontUsages.length;
|
|
785
835
|
|
|
786
|
-
let
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
836
|
+
let cssTextParts = subsetCssTextCache.get(accumulatedFontFaceDeclarations);
|
|
837
|
+
if (!cssTextParts) {
|
|
838
|
+
cssTextParts = {
|
|
839
|
+
subset: getFontUsageStylesheet(subsetFontUsages),
|
|
840
|
+
unused: getUnusedVariantsStylesheet(
|
|
841
|
+
fontUsages,
|
|
842
|
+
accumulatedFontFaceDeclarations
|
|
843
|
+
),
|
|
844
|
+
};
|
|
845
|
+
subsetCssTextCache.set(accumulatedFontFaceDeclarations, cssTextParts);
|
|
846
|
+
}
|
|
847
|
+
let subsetCssText = cssTextParts.subset;
|
|
848
|
+
const unusedVariantsCss = cssTextParts.unused;
|
|
791
849
|
if (!inlineCss && !omitFallbacks) {
|
|
792
850
|
// This can go into the same stylesheet because we won't reload all __subset suffixed families in the JS preload fallback
|
|
793
851
|
subsetCssText += unusedVariantsCss;
|
|
@@ -845,17 +903,13 @@ async function subsetFonts(
|
|
|
845
903
|
}
|
|
846
904
|
}
|
|
847
905
|
|
|
848
|
-
timings['insert subsets loop'] =
|
|
849
|
-
if (debug && console)
|
|
850
|
-
console.log(
|
|
851
|
-
`[subfont timing] insert subsets loop: ${timings['insert subsets loop']}ms`
|
|
852
|
-
);
|
|
853
|
-
phaseStart = Date.now();
|
|
906
|
+
timings['insert subsets loop'] = insertPhase.end();
|
|
854
907
|
|
|
855
908
|
if (numFontUsagesWithSubset === 0) {
|
|
856
909
|
return { fontInfo: [], timings };
|
|
857
910
|
}
|
|
858
911
|
|
|
912
|
+
const lazyFallbackPhase = trackPhase('lazy load fallback CSS');
|
|
859
913
|
const relationsToRemove = new Set();
|
|
860
914
|
|
|
861
915
|
// Lazy load the original @font-face declarations of self-hosted fonts (unless omitFallbacks)
|
|
@@ -956,12 +1010,9 @@ async function subsetFonts(
|
|
|
956
1010
|
relationsToRemove.add(fallbackHtmlStyle);
|
|
957
1011
|
}
|
|
958
1012
|
|
|
959
|
-
timings['lazy load fallback CSS'] =
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
`[subfont timing] lazy load fallback CSS: ${timings['lazy load fallback CSS']}ms`
|
|
963
|
-
);
|
|
964
|
-
phaseStart = Date.now();
|
|
1013
|
+
timings['lazy load fallback CSS'] = lazyFallbackPhase.end();
|
|
1014
|
+
|
|
1015
|
+
const removeFontFacePhase = trackPhase('remove original @font-face');
|
|
965
1016
|
|
|
966
1017
|
// Remove the original @font-face blocks, and don't leave behind empty stylesheets:
|
|
967
1018
|
const maybeEmptyCssAssets = new Set();
|
|
@@ -984,18 +1035,20 @@ async function subsetFonts(
|
|
|
984
1035
|
}
|
|
985
1036
|
}
|
|
986
1037
|
|
|
987
|
-
timings['remove original @font-face'] =
|
|
988
|
-
if (debug && console)
|
|
989
|
-
console.log(
|
|
990
|
-
`[subfont timing] remove original @font-face: ${timings['remove original @font-face']}ms`
|
|
991
|
-
);
|
|
992
|
-
phaseStart = Date.now();
|
|
1038
|
+
timings['remove original @font-face'] = removeFontFacePhase.end();
|
|
993
1039
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1040
|
+
const googleCleanupPhase = trackPhase('Google Fonts + cleanup');
|
|
1041
|
+
|
|
1042
|
+
// Async load Google Web Fonts CSS. Skip the regex findAssets scan and
|
|
1043
|
+
// the surrounding loop entirely when no Google Fonts were detected up
|
|
1044
|
+
// front — the final detach loop below still runs because other phases
|
|
1045
|
+
// (lazy fallback CSS) populate relationsToRemove.
|
|
1046
|
+
const googleFontStylesheets = hasGoogleFonts
|
|
1047
|
+
? assetGraph.findAssets({
|
|
1048
|
+
type: 'Css',
|
|
1049
|
+
url: { $regex: googleFontsCssUrlRegex },
|
|
1050
|
+
})
|
|
1051
|
+
: [];
|
|
999
1052
|
const selfHostedGoogleCssByUrl = new Map();
|
|
1000
1053
|
for (const googleFontStylesheet of googleFontStylesheets) {
|
|
1001
1054
|
const seenPages = new Set(); // Only do the work once for each font on each page
|
|
@@ -1067,16 +1120,13 @@ async function subsetFonts(
|
|
|
1067
1120
|
relation.detach();
|
|
1068
1121
|
}
|
|
1069
1122
|
|
|
1070
|
-
timings['Google Fonts + cleanup'] =
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
`[subfont timing] Google Fonts + cleanup: ${timings['Google Fonts + cleanup']}ms`
|
|
1074
|
-
);
|
|
1075
|
-
phaseStart = Date.now();
|
|
1123
|
+
timings['Google Fonts + cleanup'] = googleCleanupPhase.end();
|
|
1124
|
+
|
|
1125
|
+
const injectPhase = trackPhase('inject subset font-family into CSS/SVG');
|
|
1076
1126
|
|
|
1077
1127
|
// Use subsets in font-family:
|
|
1078
1128
|
|
|
1079
|
-
const webfontNameMap =
|
|
1129
|
+
const webfontNameMap = Object.create(null);
|
|
1080
1130
|
|
|
1081
1131
|
for (const { fontUsages } of htmlOrSvgAssetTextsWithProps) {
|
|
1082
1132
|
for (const { subsets, fontFamilies, props } of fontUsages) {
|
|
@@ -1090,6 +1140,7 @@ async function subsetFonts(
|
|
|
1090
1140
|
}
|
|
1091
1141
|
|
|
1092
1142
|
let customPropertyDefinitions; // Avoid computing this unless necessary
|
|
1143
|
+
const cssAssetsDirtiedByCustomProps = new Set();
|
|
1093
1144
|
// Inject subset font name before original webfont in SVG font-family attributes
|
|
1094
1145
|
const svgAssets = assetGraph.findAssets({ type: 'Svg' });
|
|
1095
1146
|
for (const svgAsset of svgAssets) {
|
|
@@ -1128,7 +1179,10 @@ async function subsetFonts(
|
|
|
1128
1179
|
type: 'Css',
|
|
1129
1180
|
isLoaded: true,
|
|
1130
1181
|
});
|
|
1131
|
-
|
|
1182
|
+
const parseTreeToAsset = new Map();
|
|
1183
|
+
for (const cssAsset of cssAssets) {
|
|
1184
|
+
parseTreeToAsset.set(cssAsset.parseTree, cssAsset);
|
|
1185
|
+
}
|
|
1132
1186
|
for (const cssAsset of cssAssets) {
|
|
1133
1187
|
let changesMade = false;
|
|
1134
1188
|
cssAsset.eachRuleInParseTree((cssRule) => {
|
|
@@ -1156,7 +1210,10 @@ async function subsetFonts(
|
|
|
1156
1210
|
);
|
|
1157
1211
|
if (modifiedValue !== relatedCssRule.value) {
|
|
1158
1212
|
relatedCssRule.value = modifiedValue;
|
|
1159
|
-
|
|
1213
|
+
const ownerAsset = parseTreeToAsset.get(relatedCssRule.root());
|
|
1214
|
+
if (ownerAsset) {
|
|
1215
|
+
cssAssetsDirtiedByCustomProps.add(ownerAsset);
|
|
1216
|
+
}
|
|
1160
1217
|
}
|
|
1161
1218
|
}
|
|
1162
1219
|
}
|
|
@@ -1213,68 +1270,23 @@ async function subsetFonts(
|
|
|
1213
1270
|
}
|
|
1214
1271
|
}
|
|
1215
1272
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
for (const cssAsset of cssAssets) {
|
|
1219
|
-
cssAsset.markDirty();
|
|
1220
|
-
}
|
|
1273
|
+
for (const dirtiedAsset of cssAssetsDirtiedByCustomProps) {
|
|
1274
|
+
dirtiedAsset.markDirty();
|
|
1221
1275
|
}
|
|
1222
1276
|
|
|
1223
|
-
timings['inject subset font-family'] =
|
|
1224
|
-
if (debug && console)
|
|
1225
|
-
console.log(
|
|
1226
|
-
`[subfont timing] inject subset font-family into CSS/SVG: ${timings['inject subset font-family']}ms`
|
|
1227
|
-
);
|
|
1228
|
-
phaseStart = Date.now();
|
|
1277
|
+
timings['inject subset font-family'] = injectPhase.end();
|
|
1229
1278
|
|
|
1279
|
+
const orphanCleanupPhase = trackPhase('source maps + orphan cleanup');
|
|
1230
1280
|
if (sourceMaps) {
|
|
1231
|
-
await assetGraph
|
|
1232
|
-
type: 'Css',
|
|
1233
|
-
outgoingRelations: {
|
|
1234
|
-
$where: (relations) =>
|
|
1235
|
-
relations.some((relation) => relation.type === 'CssSourceMappingUrl'),
|
|
1236
|
-
},
|
|
1237
|
-
});
|
|
1238
|
-
for (const relation of assetGraph.findRelations({
|
|
1239
|
-
type: 'SourceMapSource',
|
|
1240
|
-
})) {
|
|
1241
|
-
relation.hrefType = hrefType;
|
|
1242
|
-
}
|
|
1243
|
-
for (const relation of assetGraph.findRelations({
|
|
1244
|
-
type: 'CssSourceMappingUrl',
|
|
1245
|
-
hrefType: { $in: ['relative', 'inline'] },
|
|
1246
|
-
})) {
|
|
1247
|
-
relation.hrefType = hrefType;
|
|
1248
|
-
}
|
|
1281
|
+
await rewriteCssSourceMaps(assetGraph, hrefType);
|
|
1249
1282
|
}
|
|
1283
|
+
removeOrphanedAssets(assetGraph, potentiallyOrphanedAssets);
|
|
1284
|
+
timings['source maps + orphan cleanup'] = orphanCleanupPhase.end();
|
|
1250
1285
|
|
|
1251
|
-
for (const asset of potentiallyOrphanedAssets) {
|
|
1252
|
-
if (asset.incomingRelations.length === 0) {
|
|
1253
|
-
assetGraph.removeAsset(asset);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
timings['source maps + orphan cleanup'] = Date.now() - phaseStart;
|
|
1258
|
-
if (debug && console)
|
|
1259
|
-
console.log(
|
|
1260
|
-
`[subfont timing] source maps + orphan cleanup: ${timings['source maps + orphan cleanup']}ms`
|
|
1261
|
-
);
|
|
1262
|
-
|
|
1263
|
-
// Hand out some useful info about the detected subsets:
|
|
1264
1286
|
return {
|
|
1265
|
-
fontInfo: htmlOrSvgAssetTextsWithProps
|
|
1266
|
-
({ fontUsages, htmlOrSvgAsset }) => ({
|
|
1267
|
-
assetFileName: htmlOrSvgAsset.nonInlineAncestor.urlOrDescription,
|
|
1268
|
-
fontUsages: fontUsages.map((fontUsage) =>
|
|
1269
|
-
(({ subsets, hasFontFeatureSettings, fontFeatureTags, ...rest }) =>
|
|
1270
|
-
rest)(fontUsage)
|
|
1271
|
-
),
|
|
1272
|
-
})
|
|
1273
|
-
),
|
|
1287
|
+
fontInfo: buildFontInfoReport(htmlOrSvgAssetTextsWithProps),
|
|
1274
1288
|
timings,
|
|
1275
1289
|
};
|
|
1276
1290
|
}
|
|
1277
1291
|
|
|
1278
1292
|
module.exports = subsetFonts;
|
|
1279
|
-
// Exported for testing
|
|
1280
|
-
module.exports._escapeJsStringLiteral = escapeJsStringLiteral;
|
package/lib/subsetGeneration.js
CHANGED
|
@@ -118,9 +118,11 @@ async function getSubsetsForFontUsage(
|
|
|
118
118
|
formats,
|
|
119
119
|
seenAxisValuesByFontUrlAndAxisName,
|
|
120
120
|
cacheDir = null,
|
|
121
|
-
console = null
|
|
121
|
+
console = null,
|
|
122
|
+
debug = false
|
|
122
123
|
) {
|
|
123
124
|
const diskCache = cacheDir ? new SubsetDiskCache(cacheDir, console) : null;
|
|
125
|
+
const cacheStats = diskCache ? { hits: 0, misses: 0 } : null;
|
|
124
126
|
|
|
125
127
|
// Collect one canonical fontUsage per font URL
|
|
126
128
|
const canonicalFontUsageByUrl = new Map();
|
|
@@ -240,8 +242,10 @@ async function getSubsetsForFontUsage(
|
|
|
240
242
|
const cachedResult = diskCache ? await diskCache.get(cacheKey) : null;
|
|
241
243
|
|
|
242
244
|
if (cachedResult) {
|
|
245
|
+
if (cacheStats) cacheStats.hits++;
|
|
243
246
|
subsetPromiseMap.set(promiseId, Promise.resolve(cachedResult));
|
|
244
247
|
} else {
|
|
248
|
+
if (cacheStats) cacheStats.misses++;
|
|
245
249
|
const subsetCall = subsetFontWithGlyphs(fontBuffer, text, {
|
|
246
250
|
targetFormat,
|
|
247
251
|
glyphIds: featureGlyphIds,
|
|
@@ -279,6 +283,14 @@ async function getSubsetsForFontUsage(
|
|
|
279
283
|
)
|
|
280
284
|
);
|
|
281
285
|
|
|
286
|
+
if (cacheStats && debug && console) {
|
|
287
|
+
const total = cacheStats.hits + cacheStats.misses;
|
|
288
|
+
const pct = total > 0 ? Math.round((cacheStats.hits * 100) / total) : 0;
|
|
289
|
+
console.log(
|
|
290
|
+
`[subfont timing] subset disk cache: ${cacheStats.hits} hit${cacheStats.hits === 1 ? '' : 's'}, ${cacheStats.misses} miss${cacheStats.misses === 1 ? '' : 'es'} (${pct}% hit rate)`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
282
294
|
// Assign subset results to canonical font usages
|
|
283
295
|
for (const [, fontUsage] of canonicalFontUsageByUrl) {
|
|
284
296
|
const info = subsetInfoByFontUrl.get(fontUsage.fontUrl);
|