Skip to content

Commit 7fba48e

Browse files
Adding experimentalAdjustFallback feature to font optimization (vercel#40185)
<!-- ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [x] Make sure the linting passes by running `pnpm lint` - [ ] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples) - [ ] --> ## Feature - [x] Implements vercel#40112 - [x] Integration tests added Adds a new option to the current font optimization to enable experimental font size adjust The new `optimizeFonts` config will be ``` optimizeFonts: { inlineFonts: true, experimentalAdjustFallbacks: false, }, ``` To enable the feature, set `experimentalAdjustFallbacks: true` `optimizeFonts: false` will disable the entire feature (including inlining google font definition) Co-authored-by: JJ Kasper <[email protected]>
1 parent 8bf082a commit 7fba48e

File tree

22 files changed

+11289
-24
lines changed

22 files changed

+11289
-24
lines changed

packages/next/build/webpack-config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export function getDefineEnv({
177177
'process.env.__NEXT_STRICT_MODE': JSON.stringify(config.reactStrictMode),
178178
'process.env.__NEXT_REACT_ROOT': JSON.stringify(hasReactRoot),
179179
'process.env.__NEXT_OPTIMIZE_FONTS': JSON.stringify(
180-
config.optimizeFonts && !dev
180+
!dev && config.optimizeFonts
181181
),
182182
'process.env.__NEXT_OPTIMIZE_CSS': JSON.stringify(
183183
config.experimental.optimizeCss && !dev
@@ -1802,6 +1802,7 @@ export default async function getBaseWebpackConfig(
18021802
}
18031803
return new FontStylesheetGatheringPlugin({
18041804
isLikeServerless,
1805+
adjustFontFallbacks: config.experimental.adjustFontFallbacks,
18051806
})
18061807
})(),
18071808
new WellKnownErrorsPlugin(),

packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) {
284284
const isPreviewMode = previewData !== false
285285

286286
if (process.env.__NEXT_OPTIMIZE_FONTS) {
287-
renderOpts.optimizeFonts = true
287+
renderOpts.optimizeFonts = process.env.__NEXT_OPTIMIZE_FONTS
288288
/**
289289
* __webpack_require__.__NEXT_FONT_MANIFEST__ is added by
290290
* font-stylesheet-gathering-plugin

packages/next/build/webpack/plugins/font-stylesheet-gathering-plugin.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from 'next/dist/compiled/webpack/webpack'
66
import {
77
getFontDefinitionFromNetwork,
8+
getFontOverrideCss,
89
FontManifest,
910
} from '../../../server/font-utils'
1011
import postcss from 'postcss'
@@ -52,9 +53,17 @@ export class FontStylesheetGatheringPlugin {
5253
gatheredStylesheets: Array<string> = []
5354
manifestContent: FontManifest = []
5455
isLikeServerless: boolean
56+
adjustFontFallbacks?: boolean
5557

56-
constructor({ isLikeServerless }: { isLikeServerless: boolean }) {
58+
constructor({
59+
isLikeServerless,
60+
adjustFontFallbacks,
61+
}: {
62+
isLikeServerless: boolean
63+
adjustFontFallbacks?: boolean
64+
}) {
5765
this.isLikeServerless = isLikeServerless
66+
this.adjustFontFallbacks = adjustFontFallbacks
5867
}
5968

6069
private parserHandler = (
@@ -212,7 +221,11 @@ export class FontStylesheetGatheringPlugin {
212221

213222
this.manifestContent = []
214223
for (let promiseIndex in fontDefinitionPromises) {
215-
const css = await fontDefinitionPromises[promiseIndex]
224+
let css = await fontDefinitionPromises[promiseIndex]
225+
226+
if (this.adjustFontFallbacks) {
227+
css += getFontOverrideCss(fontStylesheets[promiseIndex], css)
228+
}
216229

217230
if (css) {
218231
try {

packages/next/export/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { PrerenderManifest } from '../build'
4141
import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
4242
import { getPagePath } from '../server/require'
4343
import { Span } from '../trace'
44+
import { FontConfig } from '../server/font-utils'
4445

4546
const exists = promisify(existsOrig)
4647

@@ -383,7 +384,7 @@ export default async function exportApp(
383384
crossOrigin: nextConfig.crossOrigin,
384385
optimizeCss: nextConfig.experimental.optimizeCss,
385386
nextScriptWorkers: nextConfig.experimental.nextScriptWorkers,
386-
optimizeFonts: nextConfig.optimizeFonts,
387+
optimizeFonts: nextConfig.optimizeFonts as FontConfig,
387388
largePageDataBytes: nextConfig.experimental.largePageDataBytes,
388389
}
389390

@@ -587,7 +588,7 @@ export default async function exportApp(
587588
subFolders,
588589
buildExport: options.buildExport,
589590
serverless: isTargetLikeServerless(nextConfig.target),
590-
optimizeFonts: nextConfig.optimizeFonts,
591+
optimizeFonts: nextConfig.optimizeFonts as FontConfig,
591592
optimizeCss: nextConfig.experimental.optimizeCss,
592593
disableOptimizedLoading:
593594
nextConfig.experimental.disableOptimizedLoading,

packages/next/export/worker.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ComponentType } from 'react'
2-
import type { FontManifest } from '../server/font-utils'
2+
import type { FontManifest, FontConfig } from '../server/font-utils'
33
import type { GetStaticProps } from '../types'
44
import type { IncomingMessage, ServerResponse } from 'http'
55
import type { DomainLocale, NextConfigComplete } from '../server/config-shared'
@@ -59,7 +59,7 @@ interface ExportPageInput {
5959
serverRuntimeConfig: { [key: string]: any }
6060
subFolders?: boolean
6161
serverless: boolean
62-
optimizeFonts: boolean
62+
optimizeFonts: FontConfig
6363
optimizeCss: any
6464
disableOptimizedLoading: any
6565
parentSpanId: any
@@ -82,7 +82,7 @@ interface RenderOpts {
8282
ampPath?: string
8383
ampValidatorPath?: string
8484
ampSkipValidation?: boolean
85-
optimizeFonts?: boolean
85+
optimizeFonts?: FontConfig
8686
disableOptimizedLoading?: boolean
8787
optimizeCss?: any
8888
fontManifest?: FontManifest
@@ -402,7 +402,7 @@ export default async function exportPage({
402402
* TODO(prateekbh@): Remove this when experimental.optimizeFonts are being cleaned up.
403403
*/
404404
if (optimizeFonts) {
405-
process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
405+
process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(optimizeFonts)
406406
}
407407
if (optimizeCss) {
408408
process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true)

packages/next/server/base-server.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { __ApiPreviewProps } from './api-utils'
22
import type { CustomRoutes } from '../lib/load-custom-routes'
33
import type { DomainLocale } from './config'
44
import type { DynamicRoutes, PageChecker, Route } from './router'
5-
import type { FontManifest } from './font-utils'
5+
import type { FontManifest, FontConfig } from './font-utils'
66
import type { LoadComponentsReturnType } from './load-components'
77
import type { RouteMatch } from '../shared/lib/router/utils/route-matcher'
88
import type { MiddlewareRouteMatch } from '../shared/lib/router/utils/middleware-route-matcher'
@@ -201,7 +201,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
201201
customServer?: boolean
202202
ampOptimizerConfig?: { [key: string]: any }
203203
basePath: string
204-
optimizeFonts: boolean
204+
optimizeFonts: FontConfig
205205
images: ImageConfigComplete
206206
fontManifest?: FontManifest
207207
disableOptimizedLoading?: boolean
@@ -381,9 +381,9 @@ export default abstract class Server<ServerOptions extends Options = Options> {
381381
ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer,
382382
basePath: this.nextConfig.basePath,
383383
images: this.nextConfig.images,
384-
optimizeFonts: !!this.nextConfig.optimizeFonts && !dev,
384+
optimizeFonts: this.nextConfig.optimizeFonts as FontConfig,
385385
fontManifest:
386-
this.nextConfig.optimizeFonts && !dev
386+
(this.nextConfig.optimizeFonts as FontConfig) && !dev
387387
? this.getFontManifest()
388388
: undefined,
389389
optimizeCss: this.nextConfig.experimental.optimizeCss,
@@ -1194,6 +1194,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
11941194
locale,
11951195
locales,
11961196
defaultLocale,
1197+
optimizeFonts: this.renderOpts.optimizeFonts,
11971198
optimizeCss: this.renderOpts.optimizeCss,
11981199
nextScriptWorkers: this.renderOpts.nextScriptWorkers,
11991200
distDir: this.distDir,

packages/next/server/config-schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ const configSchema = {
219219
experimental: {
220220
additionalProperties: false,
221221
properties: {
222+
adjustFontFallbacks: {
223+
type: 'boolean',
224+
},
222225
amp: {
223226
additionalProperties: false,
224227
properties: {

packages/next/server/config-shared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export interface ExperimentalConfig {
150150
sri?: {
151151
algorithm?: SubresourceIntegrityAlgorithm
152152
}
153+
adjustFontFallbacks?: boolean
153154
}
154155

155156
export type ExportPathMap = {
@@ -581,6 +582,7 @@ export const defaultConfig: NextConfig = {
581582
amp: undefined,
582583
urlImports: undefined,
583584
modularizeImports: undefined,
585+
adjustFontFallbacks: false,
584586
},
585587
}
586588

packages/next/server/font-utils.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import * as Log from '../build/output/log'
2-
import { GOOGLE_FONT_PROVIDER } from '../shared/lib/constants'
2+
import {
3+
GOOGLE_FONT_PROVIDER,
4+
DEFAULT_SERIF_FONT,
5+
DEFAULT_SANS_SERIF_FONT,
6+
} from '../shared/lib/constants'
7+
const googleFontsMetrics = require('./google-font-metrics.json')
38
const https = require('https')
49

510
const CHROME_UA =
@@ -11,6 +16,8 @@ export type FontManifest = Array<{
1116
content: string
1217
}>
1318

19+
export type FontConfig = boolean
20+
1421
function isGoogleFont(url: string): boolean {
1522
return url.startsWith(GOOGLE_FONT_PROVIDER)
1623
}
@@ -77,3 +84,59 @@ export function getFontDefinitionFromManifest(
7784
})?.content || ''
7885
)
7986
}
87+
88+
function parseGoogleFontName(css: string): Array<string> {
89+
const regex = /font-family: ([^;]*)/g
90+
const matches = css.matchAll(regex)
91+
const fontNames = new Set<string>()
92+
93+
for (let font of matches) {
94+
const fontFamily = font[1].replace(/^['"]|['"]$/g, '')
95+
fontNames.add(fontFamily)
96+
}
97+
98+
return [...fontNames]
99+
}
100+
101+
function calculateOverrideCSS(font: string, fontMetrics: any) {
102+
const fontName = font.toLowerCase().trim().replace(/ /g, '-')
103+
const fontKey = font.toLowerCase().trim().replace(/ /g, '')
104+
const { category, ascentOverride, descentOverride, lineGapOverride } =
105+
fontMetrics[fontKey]
106+
const fallbackFont =
107+
category === 'serif' ? DEFAULT_SERIF_FONT : DEFAULT_SANS_SERIF_FONT
108+
const ascent = (ascentOverride * 100).toFixed(2)
109+
const descent = (descentOverride * 100).toFixed(2)
110+
const lineGap = (lineGapOverride * 100).toFixed(2)
111+
112+
return `
113+
@font-face {
114+
font-family: "${fontName}-fallback";
115+
ascent-override: ${ascent}%;
116+
descent-override: ${descent}%;
117+
line-gap-override: ${lineGap}%;
118+
src: local("${fallbackFont}");
119+
}
120+
`
121+
}
122+
123+
export function getFontOverrideCss(url: string, css: string) {
124+
if (!isGoogleFont(url)) {
125+
return ''
126+
}
127+
128+
try {
129+
const fontNames = parseGoogleFontName(css)
130+
const fontMetrics = googleFontsMetrics
131+
132+
const fontCss = fontNames.reduce((cssStr, fontName) => {
133+
cssStr += calculateOverrideCSS(fontName, fontMetrics)
134+
return cssStr
135+
}, '')
136+
137+
return fontCss
138+
} catch (e) {
139+
console.log('Error getting font override values - ', e)
140+
return ''
141+
}
142+
}

0 commit comments

Comments
 (0)