From 8fdcc52854007f64f079ce3b4a45f43269b8baec Mon Sep 17 00:00:00 2001 From: Janicklas Ralph Date: Mon, 5 Apr 2021 10:47:03 -0700 Subject: [PATCH] Font optimization (#21676) Enable font optimization by default --- packages/next/build/index.ts | 2 +- packages/next/build/webpack-config.ts | 6 +-- .../font-stylesheet-gathering-plugin.ts | 37 +++++++------- packages/next/export/index.ts | 2 +- .../next/next-server/server/config-shared.ts | 2 +- .../next/next-server/server/font-utils.ts | 49 ++++++++++++------- .../next/next-server/server/next-server.ts | 8 +-- packages/next/pages/_document.tsx | 2 +- .../build-output/test/index.test.js | 2 +- .../font-optimization/pages/_document.js | 1 + .../font-optimization/test/index.test.js | 27 +++++----- 11 files changed, 78 insertions(+), 60 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 22554b4e942d4..5ec1662b98008 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -453,7 +453,7 @@ export default async function build( BUILD_MANIFEST, PRERENDER_MANIFEST, REACT_LOADABLE_MANIFEST, - config.experimental.optimizeFonts + config.optimizeFonts ? path.join( isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY, FONT_MANIFEST diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 18b06d2a9903e..4f1738c352690 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1055,7 +1055,7 @@ export default async function getBaseWebpackConfig( config.experimental.reactMode ), 'process.env.__NEXT_OPTIMIZE_FONTS': JSON.stringify( - config.experimental.optimizeFonts && !dev + config.optimizeFonts && !dev ), 'process.env.__NEXT_OPTIMIZE_IMAGES': JSON.stringify( config.experimental.optimizeImages @@ -1168,7 +1168,7 @@ export default async function getBaseWebpackConfig( distDir, }), new ProfilingPlugin(), - config.experimental.optimizeFonts && + config.optimizeFonts && !dev && isServer && (function () { @@ -1252,7 +1252,7 @@ export default async function getBaseWebpackConfig( plugins: config.experimental.plugins, reactStrictMode: config.reactStrictMode, reactMode: config.experimental.reactMode, - optimizeFonts: config.experimental.optimizeFonts, + optimizeFonts: config.optimizeFonts, optimizeImages: config.experimental.optimizeImages, optimizeCss: config.experimental.optimizeCss, scrollRestoration: config.experimental.scrollRestoration, diff --git a/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts index ba1aad7327fe8..c3964989acff3 100644 --- a/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts +++ b/packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts @@ -16,20 +16,16 @@ import { OPTIMIZED_FONT_PROVIDERS, } from '../../../next-server/lib/constants' -async function minifyCss(css: string): Promise { - return new Promise((resolve) => - postcss([ - minifier({ - excludeAll: true, - discardComments: true, - normalizeWhitespace: { exclude: false }, - }), - ]) - .process(css, { from: undefined }) - .then((res) => { - resolve(res.css) - }) - ) +function minifyCss(css: string): Promise { + return postcss([ + minifier({ + excludeAll: true, + discardComments: true, + normalizeWhitespace: { exclude: false }, + }), + ]) + .process(css, { from: undefined }) + .then((res) => res.css) } export class FontStylesheetGatheringPlugin { @@ -173,11 +169,14 @@ export class FontStylesheetGatheringPlugin { this.manifestContent = [] for (let promiseIndex in fontDefinitionPromises) { const css = await fontDefinitionPromises[promiseIndex] - const content = await minifyCss(css) - this.manifestContent.push({ - url: this.gatheredStylesheets[promiseIndex], - content, - }) + + if (css) { + const content = await minifyCss(css) + this.manifestContent.push({ + url: this.gatheredStylesheets[promiseIndex], + content, + }) + } } if (!isWebpack5) { compilation.assets[FONT_MANIFEST] = new sources.RawSource( diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 5513a1b83aa21..5cdb87751fca2 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -534,7 +534,7 @@ export default async function exportApp( subFolders, buildExport: options.buildExport, serverless: isTargetLikeServerless(nextConfig.target), - optimizeFonts: nextConfig.experimental.optimizeFonts, + optimizeFonts: nextConfig.optimizeFonts, optimizeImages: nextConfig.experimental.optimizeImages, optimizeCss: nextConfig.experimental.optimizeCss, parentSpanId: pageExportSpan.id, diff --git a/packages/next/next-server/server/config-shared.ts b/packages/next/next-server/server/config-shared.ts index 19b7df80da873..ddb2a31c5fbbd 100644 --- a/packages/next/next-server/server/config-shared.ts +++ b/packages/next/next-server/server/config-shared.ts @@ -93,6 +93,7 @@ export const defaultConfig: NextConfig = { trailingSlash: false, i18n: null, productionBrowserSourceMaps: false, + optimizeFonts: true, experimental: { cpus: Math.max( 1, @@ -105,7 +106,6 @@ export const defaultConfig: NextConfig = { reactMode: 'legacy', workerThreads: false, pageEnv: false, - optimizeFonts: false, optimizeImages: false, optimizeCss: false, scrollRestoration: false, diff --git a/packages/next/next-server/server/font-utils.ts b/packages/next/next-server/server/font-utils.ts index 6120ff10be27b..4d4b25dbe9425 100644 --- a/packages/next/next-server/server/font-utils.ts +++ b/packages/next/next-server/server/font-utils.ts @@ -1,3 +1,4 @@ +import * as Log from '../../build/output/log' const https = require('https') const CHROME_UA = @@ -10,24 +11,28 @@ export type FontManifest = Array<{ }> function getFontForUA(url: string, UA: string): Promise { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { let rawData: any = '' - https.get( - url, - { - headers: { - 'user-agent': UA, + https + .get( + url, + { + headers: { + 'user-agent': UA, + }, }, - }, - (res: any) => { - res.on('data', (chunk: any) => { - rawData += chunk - }) - res.on('end', () => { - resolve(rawData.toString('utf8')) - }) - } - ) + (res: any) => { + res.on('data', (chunk: any) => { + rawData += chunk + }) + res.on('end', () => { + resolve(rawData.toString('utf8')) + }) + } + ) + .on('error', (e: Error) => { + reject(e) + }) }) } @@ -39,8 +44,16 @@ export async function getFontDefinitionFromNetwork( * The order of IE -> Chrome is important, other wise chrome starts loading woff1. * CSS cascading 🤷‍♂️. */ - result += await getFontForUA(url, IE_UA) - result += await getFontForUA(url, CHROME_UA) + try { + result += await getFontForUA(url, IE_UA) + result += await getFontForUA(url, CHROME_UA) + } catch (e) { + Log.warn( + `Failed to download the stylesheet for ${url}. Skipped optimizing this font.` + ) + return '' + } + return result } diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index ca02ba875a784..420205f5a4b5a 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -205,9 +205,9 @@ export default class Server { ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer, basePath: this.nextConfig.basePath, images: JSON.stringify(this.nextConfig.images), - optimizeFonts: !!this.nextConfig.experimental.optimizeFonts && !dev, + optimizeFonts: !!this.nextConfig.optimizeFonts && !dev, fontManifest: - this.nextConfig.experimental.optimizeFonts && !dev + this.nextConfig.optimizeFonts && !dev ? requireFontManifest(this.distDir, this._isLikeServerless) : null, optimizeImages: !!this.nextConfig.experimental.optimizeImages, @@ -272,9 +272,9 @@ export default class Server { /** * This sets environment variable to be used at the time of SSR by head.tsx. - * Using this from process.env allows targetting both serverless and SSR by calling + * Using this from process.env allows targeting both serverless and SSR by calling * `process.env.__NEXT_OPTIMIZE_IMAGES`. - * TODO(atcastle@): Remove this when experimental.optimizeImages are being clened up. + * TODO(atcastle@): Remove this when experimental.optimizeImages are being cleaned up. */ if (this.renderOpts.optimizeFonts) { process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true) diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index dea39bef67cc2..20e521506cc4e 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -398,7 +398,7 @@ export class Head extends Component< }) head = cssPreloads.concat(otherHeadElements) } - let children = this.props.children + let children = React.Children.toArray(this.props.children).filter(Boolean) // show a warning if Head contains (only in development) if (process.env.NODE_ENV !== 'production') { children = React.Children.map(children, (child: any) => { diff --git a/test/integration/build-output/test/index.test.js b/test/integration/build-output/test/index.test.js index 5c63cc1a06939..ea921ab92aefd 100644 --- a/test/integration/build-output/test/index.test.js +++ b/test/integration/build-output/test/index.test.js @@ -99,7 +99,7 @@ describe('Build Output', () => { expect(parseFloat(indexFirstLoad)).toBeCloseTo(65.3, 1) expect(indexFirstLoad.endsWith('kB')).toBe(true) - expect(parseFloat(err404Size) - 3.7).toBeLessThanOrEqual(0) + expect(parseFloat(err404Size)).toBeCloseTo(3.7, 1) expect(err404Size.endsWith('kB')).toBe(true) expect(parseFloat(err404FirstLoad)).toBeCloseTo(68.5, 0) diff --git a/test/integration/font-optimization/pages/_document.js b/test/integration/font-optimization/pages/_document.js index 59d8fe75cd4c4..f3f300181f324 100644 --- a/test/integration/font-optimization/pages/_document.js +++ b/test/integration/font-optimization/pages/_document.js @@ -19,6 +19,7 @@ export default class MyDocument extends Document { rel="stylesheet" href="https://fonts.googleapis.com/css?family=Voces" /> + {false && <script data-href="test"></script>} </Head> <body> <Main /> diff --git a/test/integration/font-optimization/test/index.test.js b/test/integration/font-optimization/test/index.test.js index 358e48c1dd643..5c2323af2a1ba 100644 --- a/test/integration/font-optimization/test/index.test.js +++ b/test/integration/font-optimization/test/index.test.js @@ -10,6 +10,7 @@ import { initNextServerScript, } from 'next-test-utils' import fs from 'fs-extra' +import cheerio from 'cheerio' jest.setTimeout(1000 * 60 * 2) @@ -54,13 +55,21 @@ function runTests() { it('should pass nonce to the inlined font definition', async () => { const html = await renderViaHTTP(appPort, '/nonce') + const $ = cheerio.load(html) expect(await fsExists(builtPage('font-manifest.json'))).toBe(true) - expect(html).toContain( - '<link rel="stylesheet" nonce="VmVyY2Vs" data-href="https://fonts.googleapis.com/css2?family=Modak"/>' + + const link = $( + 'link[rel="stylesheet"][data-href="https://fonts.googleapis.com/css2?family=Modak"]' ) - expect(html).toMatch( - /<style data-href="https:\/\/fonts\.googleapis\.com\/css2\?family=Modak" nonce="VmVyY2Vs">.*<\/style>/ + const nonce = link.attr('nonce') + const style = $( + 'style[data-href="https://fonts.googleapis.com/css2?family=Modak"]' ) + const styleNonce = style.attr('nonce') + + expect(link).toBeDefined() + expect(nonce).toBe('VmVyY2Vs') + expect(styleNonce).toBe('VmVyY2Vs') }) it('should inline the google fonts for static pages with Next/Head', async () => { @@ -117,11 +126,7 @@ function runTests() { describe('Font optimization for SSR apps', () => { beforeAll(async () => { - await fs.writeFile( - nextConfig, - `module.exports = { experimental: {optimizeFonts: true} }`, - 'utf8' - ) + await fs.writeFile(nextConfig, `module.exports = {}`, 'utf8') if (fs.pathExistsSync(join(appDir, '.next'))) { await fs.remove(join(appDir, '.next')) @@ -140,7 +145,7 @@ describe('Font optimization for serverless apps', () => { beforeAll(async () => { await fs.writeFile( nextConfig, - `module.exports = { target: 'serverless', experimental: {optimizeFonts: true} }`, + `module.exports = { target: 'serverless' }`, 'utf8' ) await nextBuild(appDir) @@ -157,7 +162,7 @@ describe('Font optimization for emulated serverless apps', () => { beforeAll(async () => { await fs.writeFile( nextConfig, - `module.exports = { target: 'experimental-serverless-trace', experimental: {optimizeFonts: true} }`, + `module.exports = { target: 'experimental-serverless-trace' }`, 'utf8' ) await nextBuild(appDir)