Flutter Webで日本語が一瞬
文字化けする問題を
font-display:blockで解決した話
Flutter WebのHTMLレンダラーはDartのFontLoaderではなくブラウザ自身のCSSフォントシステムで文字を描画するため、日本語フォントの読み込みが1テンポ遅れて豆腐や文字化けが一瞬見えることがある。原因はfont-display: swapで先に代替フォントのまま描画してしまうことで、CSS側のfont-display: blockとDart側でのフォント先読みを組み合わせる2段構えで解消した。
なぜDart側のFontLoaderだけでは間に合わないのか
Flutter WebはCanvasKitではなくHTMLレンダラーを使う場合、実際の文字描画をブラウザのCSSエンジンに委譲する。そのためDart側でFontLoader('NotoSansJP').load()を待っても、ウィジェットツリーの初回構築とブラウザのフォント読み込みタイミングは別物として扱われる。
- Google FontsのSyne・DM SansはLatin用グリフしか持たず漢字・かなを描画できない
- 日本語フォールバックとして
NotoSansJP-VariableFont_wght.ttfを使うが、読み込み完了前に描画が始まると文字化けが一瞬表示される - Web版のビルドではuser assetsが
assets/assets/fonts/...という二重パスになり、CSSから直接参照できない
font-display: swap と block の違い
swapは代替フォントで即座に描画してから本フォントに差し替えるため速く見えるが、日本語では一瞬の文字化けが必ず発生する。blockは本フォントの読み込みを待ってから描画するため、文字化けした状態がユーザーの目に触れない。
| 設定値 | 初回描画 | 文字化けの見え方 |
|---|---|---|
font-display: swap | 即座に代替フォントで描画 | 本フォント読み込み完了まで文字化けが見える |
font-display: block | 本フォント読み込み完了まで描画をブロック | 文字化けした状態が画面に出ない |
2段構えの対策
CSS側で描画をブロックし、Dart側でもrunApp()より前にフォント読み込みを完了させることで、文字化けが表示される経路を両方とも塞ぐ。
web/index.htmlの<head>に<link rel="preload">でフォントを先読みし、@font-faceにfont-display: blockを指定- Dart側の
main()でWidgetsFlutterBinding.ensureInitialized()直後にFontLoaderで読み込み、await fontLoader.load()をrunApp()より前に完了させる
<link rel="preload" href="fonts/NotoSansJP-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin>
<style>
@font-face {
font-family: 'NotoSansJP';
src: url('fonts/NotoSansJP-VariableFont_wght.ttf') format('truetype');
font-weight: 100 900;
font-display: block;
}
</style>
手順4(CSS宣言)だけでも手順5(Dart側の先読み)だけでも、単独では文字化けが再現することがある。両方を揃えて初めて「文字化けゼロ」が安定する。
39本のアプリ全てに同じパターンを徹底する
この対策はSEADICEの全Flutterアプリ共通のチェックリストとして固定化し、新規アプリを作るたびに同じ手順を機械的に繰り返すだけにしている。
assets/fonts/とweb/fonts/の両方にフォントファイルを配置(Webは二重パス回避のため直置き必須)pubspec.yamlでフォントファミリーとして登録ThemeDataのtextThemeにfontFamilyFallback: ['NotoSansJP']を設定
Related
新規アプリ公開後の「配線忘れ」をレジストリで自動化した話 →
← ブログ一覧に戻る