Skip to content

perf(fonts): self-host Inter via next/font instead of remote stylesheets - #755

Open
dgaponov wants to merge 1 commit into
mainfrom
seo/render-blocking-css
Open

perf(fonts): self-host Inter via next/font instead of remote stylesheets#755
dgaponov wants to merge 1 commit into
mainfrom
seo/render-blocking-css

Conversation

@dgaponov

@dgaponov dgaponov commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Inter подключался через CSS @import url(https://fonts.googleapis.com/...). Такой импорт обнаруживается только после парсинга импортирующего стиля, поэтому цепочка была последовательной: документ → CSS → googleapis → gstatic, с DNS и TLS на каждый сторонний домен (~0.9–1.2 с блокировки на мобильном).

Источников было три, а не один: vendors.scss, прямой импорт uikit/styles/fonts.css в _app и он же транзитивно через blog-constructor. Последний заглушён алиасом, иначе возвращается через зависимость.

Стало: 0 сторонних доменов, шрифты грузятся до стилей по preload. Бонусом next/font даёт size-adjusted fallback, уменьшающий сдвиг макета.

Две детали: шрифт нужно объявлять в _app, а не _document (иначе CSS не эмитится и Inter молча пропадает), и override .g-root использует удвоенный селектор — правило uikit имеет ту же специфичность и выигрывает по порядку.

Это только половина находки про render-blocking. Глобальный CSS-чанк на 522 КБ не тронут.

🤖 Generated with Claude Code

Inter was pulled in by CSS `@import url(https://fonts.googleapis.com/...)`
statements. A CSS @import is only discovered after the importing stylesheet
has been downloaded and parsed, so the critical path was serialized:
document -> app CSS -> fonts.googleapis.com -> fonts.gstatic.com, with a DNS
lookup and TLS handshake for each third-party origin. Under mobile throttling
this measured as roughly 0.9-1.2s of render-blocking time.

There were three sources, not one:
- src/vendors.scss (Inter wght@200)
- @gravity-ui/uikit/styles/fonts.css, imported directly by _app
- the same uikit file again, imported transitively by
  @gravity-ui/blog-constructor/styles/styles.css

Inter is now downloaded at build time and served from our own origin with a
preload hint. The two extra origins and both extra round trips are gone; the
font files are now requested before the stylesheets rather than after them.
next/font also generates a size-adjusted fallback face, which reduces layout
shift while the font loads.

Notes on the implementation:
- The font must be declared in _app, not _document. Declaring it in _document
  produced the class names but emitted no CSS and downloaded no font files,
  which would have silently dropped Inter site-wide.
- The uikit stylesheet is aliased to an empty file so the transitive import
  cannot reintroduce the remote request.
- The .g-root override uses a doubled selector. uikit's own .g-root rule has
  equal specificity and wins on source order, which left sandbox pages
  resolving a literal "Inter" that no longer exists as a family name.

Verified in a production build and a real browser: zero requests to
googleapis/gstatic on landing, blog, component and sandbox pages; Inter 200,
400 and 600 loaded from our origin and applied to headings; sandbox pages
still render Inter; ru pages still render Cyrillic. All page types return 200.
Build, lint and typecheck pass. E2E not run - Playwright browsers are not
installed locally.

This addresses the font half of the render-blocking finding only. The 522 kB
(68 kB gzip) global CSS chunk is untouched and still render-blocking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dgaponov
dgaponov marked this pull request as ready for review September 11, 2026 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant