Skip to content

Commit

Permalink
Adding experimentalAdjustFallback feature to font optimization (#40185)
Browse files Browse the repository at this point in the history
<!--


## 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 #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 <jj@jjsweb.site>
  • Loading branch information
janicklas-ralph and ijjk committed Sep 16, 2022
1 parent 8bf082a commit 7fba48e
Show file tree
Hide file tree
Showing 22 changed files with 11,289 additions and 24 deletions.
3 changes: 2 additions & 1 deletion packages/next/build/webpack-config.ts
Expand Up @@ -177,7 +177,7 @@ export function getDefineEnv({
'process.env.__NEXT_STRICT_MODE': JSON.stringify(config.reactStrictMode),
'process.env.__NEXT_REACT_ROOT': JSON.stringify(hasReactRoot),
'process.env.__NEXT_OPTIMIZE_FONTS': JSON.stringify(
config.optimizeFonts && !dev
!dev && config.optimizeFonts
),
'process.env.__NEXT_OPTIMIZE_CSS': JSON.stringify(
config.experimental.optimizeCss && !dev
Expand Down Expand Up @@ -1802,6 +1802,7 @@ export default async function getBaseWebpackConfig(
}
return new FontStylesheetGatheringPlugin({
isLikeServerless,
adjustFontFallbacks: config.experimental.adjustFontFallbacks,
})
})(),
new WellKnownErrorsPlugin(),
Expand Down
Expand Up @@ -284,7 +284,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) {
const isPreviewMode = previewData !== false

if (process.env.__NEXT_OPTIMIZE_FONTS) {
renderOpts.optimizeFonts = true
renderOpts.optimizeFonts = process.env.__NEXT_OPTIMIZE_FONTS
/**
* __webpack_require__.__NEXT_FONT_MANIFEST__ is added by
* font-stylesheet-gathering-plugin
Expand Down
Expand Up @@ -5,6 +5,7 @@ import {
} from 'next/dist/compiled/webpack/webpack'
import {
getFontDefinitionFromNetwork,
getFontOverrideCss,
FontManifest,
} from '../../../server/font-utils'
import postcss from 'postcss'
Expand Down Expand Up @@ -52,9 +53,17 @@ export class FontStylesheetGatheringPlugin {
gatheredStylesheets: Array<string> = []
manifestContent: FontManifest = []
isLikeServerless: boolean
adjustFontFallbacks?: boolean

constructor({ isLikeServerless }: { isLikeServerless: boolean }) {
constructor({
isLikeServerless,
adjustFontFallbacks,
}: {
isLikeServerless: boolean
adjustFontFallbacks?: boolean
}) {
this.isLikeServerless = isLikeServerless
this.adjustFontFallbacks = adjustFontFallbacks
}

private parserHandler = (
Expand Down Expand Up @@ -212,7 +221,11 @@ export class FontStylesheetGatheringPlugin {

this.manifestContent = []
for (let promiseIndex in fontDefinitionPromises) {
const css = await fontDefinitionPromises[promiseIndex]
let css = await fontDefinitionPromises[promiseIndex]

if (this.adjustFontFallbacks) {
css += getFontOverrideCss(fontStylesheets[promiseIndex], css)
}

if (css) {
try {
Expand Down
5 changes: 3 additions & 2 deletions packages/next/export/index.ts
Expand Up @@ -41,6 +41,7 @@ import { PrerenderManifest } from '../build'
import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
import { getPagePath } from '../server/require'
import { Span } from '../trace'
import { FontConfig } from '../server/font-utils'

const exists = promisify(existsOrig)

Expand Down Expand Up @@ -383,7 +384,7 @@ export default async function exportApp(
crossOrigin: nextConfig.crossOrigin,
optimizeCss: nextConfig.experimental.optimizeCss,
nextScriptWorkers: nextConfig.experimental.nextScriptWorkers,
optimizeFonts: nextConfig.optimizeFonts,
optimizeFonts: nextConfig.optimizeFonts as FontConfig,
largePageDataBytes: nextConfig.experimental.largePageDataBytes,
}

Expand Down Expand Up @@ -587,7 +588,7 @@ export default async function exportApp(
subFolders,
buildExport: options.buildExport,
serverless: isTargetLikeServerless(nextConfig.target),
optimizeFonts: nextConfig.optimizeFonts,
optimizeFonts: nextConfig.optimizeFonts as FontConfig,
optimizeCss: nextConfig.experimental.optimizeCss,
disableOptimizedLoading:
nextConfig.experimental.disableOptimizedLoading,
Expand Down
8 changes: 4 additions & 4 deletions packages/next/export/worker.ts
@@ -1,5 +1,5 @@
import type { ComponentType } from 'react'
import type { FontManifest } from '../server/font-utils'
import type { FontManifest, FontConfig } from '../server/font-utils'
import type { GetStaticProps } from '../types'
import type { IncomingMessage, ServerResponse } from 'http'
import type { DomainLocale, NextConfigComplete } from '../server/config-shared'
Expand Down Expand Up @@ -59,7 +59,7 @@ interface ExportPageInput {
serverRuntimeConfig: { [key: string]: any }
subFolders?: boolean
serverless: boolean
optimizeFonts: boolean
optimizeFonts: FontConfig
optimizeCss: any
disableOptimizedLoading: any
parentSpanId: any
Expand All @@ -82,7 +82,7 @@ interface RenderOpts {
ampPath?: string
ampValidatorPath?: string
ampSkipValidation?: boolean
optimizeFonts?: boolean
optimizeFonts?: FontConfig
disableOptimizedLoading?: boolean
optimizeCss?: any
fontManifest?: FontManifest
Expand Down Expand Up @@ -402,7 +402,7 @@ export default async function exportPage({
* TODO(prateekbh@): Remove this when experimental.optimizeFonts are being cleaned up.
*/
if (optimizeFonts) {
process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(optimizeFonts)
}
if (optimizeCss) {
process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true)
Expand Down
9 changes: 5 additions & 4 deletions packages/next/server/base-server.ts
Expand Up @@ -2,7 +2,7 @@ import type { __ApiPreviewProps } from './api-utils'
import type { CustomRoutes } from '../lib/load-custom-routes'
import type { DomainLocale } from './config'
import type { DynamicRoutes, PageChecker, Route } from './router'
import type { FontManifest } from './font-utils'
import type { FontManifest, FontConfig } from './font-utils'
import type { LoadComponentsReturnType } from './load-components'
import type { RouteMatch } from '../shared/lib/router/utils/route-matcher'
import type { MiddlewareRouteMatch } from '../shared/lib/router/utils/middleware-route-matcher'
Expand Down Expand Up @@ -201,7 +201,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
customServer?: boolean
ampOptimizerConfig?: { [key: string]: any }
basePath: string
optimizeFonts: boolean
optimizeFonts: FontConfig
images: ImageConfigComplete
fontManifest?: FontManifest
disableOptimizedLoading?: boolean
Expand Down Expand Up @@ -381,9 +381,9 @@ export default abstract class Server<ServerOptions extends Options = Options> {
ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer,
basePath: this.nextConfig.basePath,
images: this.nextConfig.images,
optimizeFonts: !!this.nextConfig.optimizeFonts && !dev,
optimizeFonts: this.nextConfig.optimizeFonts as FontConfig,
fontManifest:
this.nextConfig.optimizeFonts && !dev
(this.nextConfig.optimizeFonts as FontConfig) && !dev
? this.getFontManifest()
: undefined,
optimizeCss: this.nextConfig.experimental.optimizeCss,
Expand Down Expand Up @@ -1194,6 +1194,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
locale,
locales,
defaultLocale,
optimizeFonts: this.renderOpts.optimizeFonts,
optimizeCss: this.renderOpts.optimizeCss,
nextScriptWorkers: this.renderOpts.nextScriptWorkers,
distDir: this.distDir,
Expand Down
3 changes: 3 additions & 0 deletions packages/next/server/config-schema.ts
Expand Up @@ -219,6 +219,9 @@ const configSchema = {
experimental: {
additionalProperties: false,
properties: {
adjustFontFallbacks: {
type: 'boolean',
},
amp: {
additionalProperties: false,
properties: {
Expand Down
2 changes: 2 additions & 0 deletions packages/next/server/config-shared.ts
Expand Up @@ -150,6 +150,7 @@ export interface ExperimentalConfig {
sri?: {
algorithm?: SubresourceIntegrityAlgorithm
}
adjustFontFallbacks?: boolean
}

export type ExportPathMap = {
Expand Down Expand Up @@ -581,6 +582,7 @@ export const defaultConfig: NextConfig = {
amp: undefined,
urlImports: undefined,
modularizeImports: undefined,
adjustFontFallbacks: false,
},
}

Expand Down
65 changes: 64 additions & 1 deletion packages/next/server/font-utils.ts
@@ -1,5 +1,10 @@
import * as Log from '../build/output/log'
import { GOOGLE_FONT_PROVIDER } from '../shared/lib/constants'
import {
GOOGLE_FONT_PROVIDER,
DEFAULT_SERIF_FONT,
DEFAULT_SANS_SERIF_FONT,
} from '../shared/lib/constants'
const googleFontsMetrics = require('./google-font-metrics.json')
const https = require('https')

const CHROME_UA =
Expand All @@ -11,6 +16,8 @@ export type FontManifest = Array<{
content: string
}>

export type FontConfig = boolean

function isGoogleFont(url: string): boolean {
return url.startsWith(GOOGLE_FONT_PROVIDER)
}
Expand Down Expand Up @@ -77,3 +84,59 @@ export function getFontDefinitionFromManifest(
})?.content || ''
)
}

function parseGoogleFontName(css: string): Array<string> {
const regex = /font-family: ([^;]*)/g
const matches = css.matchAll(regex)
const fontNames = new Set<string>()

for (let font of matches) {
const fontFamily = font[1].replace(/^['"]|['"]$/g, '')
fontNames.add(fontFamily)
}

return [...fontNames]
}

function calculateOverrideCSS(font: string, fontMetrics: any) {
const fontName = font.toLowerCase().trim().replace(/ /g, '-')
const fontKey = font.toLowerCase().trim().replace(/ /g, '')
const { category, ascentOverride, descentOverride, lineGapOverride } =
fontMetrics[fontKey]
const fallbackFont =
category === 'serif' ? DEFAULT_SERIF_FONT : DEFAULT_SANS_SERIF_FONT
const ascent = (ascentOverride * 100).toFixed(2)
const descent = (descentOverride * 100).toFixed(2)
const lineGap = (lineGapOverride * 100).toFixed(2)

return `
@font-face {
font-family: "${fontName}-fallback";
ascent-override: ${ascent}%;
descent-override: ${descent}%;
line-gap-override: ${lineGap}%;
src: local("${fallbackFont}");
}
`
}

export function getFontOverrideCss(url: string, css: string) {
if (!isGoogleFont(url)) {
return ''
}

try {
const fontNames = parseGoogleFontName(css)
const fontMetrics = googleFontsMetrics

const fontCss = fontNames.reduce((cssStr, fontName) => {
cssStr += calculateOverrideCSS(fontName, fontMetrics)
return cssStr
}, '')

return fontCss
} catch (e) {
console.log('Error getting font override values - ', e)
return ''
}
}

0 comments on commit 7fba48e

Please sign in to comment.