Gejala antarmuka berkedip (UI flicker) dan elemen visual yang melompat (layout shift) saat aplikasi React Native dibuka hampir selalu berakar pada proses asynchronous font hydration. Ketika komponen teks dirender pertama kali menggunakan font cadangan (fallback/system font), pergantian mendadak ke custom font memicu pergeseran dimensi visual yang merusak user experience.
Akar Masalah: Yoga Layout Engine dan Perbedaan Metrik Glyph
React Native mendelegasikan kalkulasi kalkulasi tata letak flexbox ke Yoga engine. Pada level native, elemen teks diukur melalui fungsi pengukuran intrinsik platform (seperti TextLayout pada Android atau NSLayoutManager/CoreText pada iOS). Dimensi intrinsik ini dilaporkan kembali ke node Yoga yang bersangkutan.
Setiap tipografi memiliki glyph metrics yang unik, mencakup nilai ascent, descent, cap-height, dan line-gap. Ketika aplikasi pertama kali melakukan mount:
- Node teks mengukur dimensi menggunakan font sistem (misalnya Roboto di Android atau San Francisco di iOS).
- Yoga menghitung posisi pohon komponen (bounding box) dan mengeksekusi layout pass awal.
- Aset font kustom (seperti Inter atau Poppins) selesai dimuat secara asinkron dari disk atau bundle assets.
- Properti font keluarga diterapkan, native text measurement memicu
markDirty()pada Yoga node karena dimensi teks berubah. - Yoga melakukan kalkulasi ulang (relayout pass). Elemen di bawah atau di samping teks terdorong mendadak, menghasilkan Cumulative Layout Shift (CLS) pada mobile.
Solusi 1: Gating Mount Tree Menggunakan Native Splash Screen
Solusi paling stabil untuk mencegah FOUT (Flash of Unstyled Text) dan layout jump adalah menahan render view tree hingga seluruh aset font terkonfirmasi siap. Pendekatan ini dilakukan dengan menahan native splash screen tetap terlihat (menggunakan library seperti react-native-bootsplash atau expo-splash-screen).
Berikut pola implementasi TypeScript untuk menahan mounting komponen UI sampai font selesai dihidrasi:
import React, { useEffect, useState } from 'react';
import { View, StyleSheet } from 'react-native';
import * as Font from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
SplashScreen.preventAutoHideAsync().catch(() => {
/* Handle native splash lock error */
});
interface AppBootstrapProps {
children: React.ReactNode;
}
export const AppBootstrap: React.FC<AppBootstrapProps> = ({ children }) => {
const [fontsReady, setFontsReady] = useState<boolean>(false);
useEffect(() => {
async function loadResources(): Promise<void> {
try {
await Font.loadAsync({
'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'),
'Inter-Bold': require('./assets/fonts/Inter-Bold.ttf'),
});
} catch (error) {
console.warn('Font loading failed:', error);
} finally {
setFontsReady(true);
}
}
loadResources();
}, []);
useEffect(() => {
if (fontsReady) {
SplashScreen.hideAsync().catch(() => {});
}
}, [fontsReady]);
if (!fontsReady) {
return null;
}
return <View style={styles.root}>{children}</View>;
};
const styles = StyleSheet.create({
root: {
flex: 1,
},
});
Catatan: Menghasilkanreturn null;saat!fontsReadymencegah Yoga membuat pohon node sama sekali. Layout pass pertama baru dieksekusi tepat saat custom font sudah terdaftar di native layer.
Solusi 2: Normalisasi Android Metrics dengan includeFontPadding: false
Android menambahkan padding vertikal bawaan pada komponen TextView melalui atribut includeFontPadding. Atribut ini aktif secara default (true) untuk mengakomodasi glif aksen ekstrem pada bahasa tertentu. Namun, perilaku ini menyebabkan ketidakcocokan metrik yang ekstrem antara font sistem bawaan Android dan font kustom berformat OTF/TTF modern.
Untuk mencegah lompatan tinggi vertikal antar platform dan saat transisi rendering, nonaktifkan opsi ini di seluruh text styling:
import React from 'react';
import { Text as RNText, TextProps, StyleSheet } from 'react-native';
interface BaseTextProps extends TextProps {
variant?: 'regular' | 'bold';
}
export const AppText: React.FC<BaseTextProps> = ({
style,
variant = 'regular',
children,
...rest
}) => {
const fontStyle = variant === 'bold' ? styles.bold : styles.regular;
return (
<RNText style={[styles.base, fontStyle, style]} {...rest}>
{children}
</RNText>
);
};
const styles = StyleSheet.create({
base: {
// Menghilangkan kalkulasi vertikal ekstra bawaan Android
includeFontPadding: false,
textAlignVertical: 'center',
},
regular: {
fontFamily: 'Inter-Regular',
},
bold: {
fontFamily: 'Inter-Bold',
},
});
Solusi 3: Mengunci Bounding Box Jika Splash Gating Tidak Diinginkan
Jika aplikasi memiliki arsitektur yang menuntut interaktivitas instan tanpa memblokir layar menggunakan splash screen, layout shift dapat dicegah dengan mengunci dimensi wadah penampung teks.
- Terapkan explicit lineHeight: Berikan
lineHeighttetap yang proporsional terhadapfontSize(misalnyafontSize: 16,lineHeight: 24). Ini memaksa native platform menggunakan baseline height yang konstan baik pada font fallback maupun custom font. - Tentukan minHeight pada kontainer teks: Berikan
minHeightpada container pembungkus teks dinamis (misalnya baris heading atau tombol CTA). Yoga tidak akan mengecilkan atau memperbesar ukuran node pembungkus ketika metrik glif berubah beberapa milidetik kemudian. - Hindari layout flow bergantung pada teks inline: Jangan letakkan ikon atau badge yang posisinya bersandar langsung pada alignment inline teks asinkron tanpa pembungkus yang memiliki fixed dimensions.
Ringkasan Keputusan Implementasi
Gunakan kombinasi Native Splash Screen Gating untuk memastikan seluruh runtime JavaScript dan aset font sinkron sebelum tampilan pertama digambar. Tambahkan pembungkus AppText dengan konfigurasi includeFontPadding: false dan lineHeight eksplisit untuk menghilangkan ketidaksinkronan render vertikal antar platform selamanya.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!