Tachyon 작동 방식
Tachyon은 사용자의 웹 브라우저에 내장된 기능을 활용하여 사용자가 <a href="..."></a> 태그에 커서를 50밀리초 이상 올려놓으면 콘텐츠를 미리 로드하는 <link rel="prerender" href="..."> 태그를 생성합니다(기본값).
기본적으로 사용자가 링크를 실제로 클릭/탭하기 전에 방문하려는 페이지의 로딩을 시작하도록 브라우저에 지시합니다. 이는 웹 브라우저가 백그라운드에서 준비를 시작하도록 지시합니다.
사용자가 실제로 링크를 클릭하고 다음 페이지로 이동할 준비가 되면 해당 페이지는 이미 준비되어 프레임으로 가져와 페이지 로드 시간이 훨씬 빨라집니다.
이유; 방법
Tachyon은 단순성을 핵심으로 설계되었으며, 이는 결코 우연이 아닙니다. 단순성에 중점을 두었기 때문에 관리자부터 최종 사용자까지 Tachyon을 사용하는 모든 사람이 성능, 확장성, 유지보수성, 보안 및 사용 편의성에서 이점을 누릴 수 있습니다.
다른 대안에 비해 Tachyon이 개선한 주요 사항 중 하나는 일반적인 <link rel="prefetch" href="..."> 대신 <link rel="prerender" href="...">를 사용하여 페이지 로드가 훨씬 빨라졌다는 점입니다. 프리페치는 페이지를 다운로드하기만 하고 프리렌더는 페이지를 다운로드하여 렌더링을 시작한다는 점에서 두 방법의 차이는 자명합니다.
또한 Tachyon은 클릭 가능성이 높은 페이지만 미리 로드하고 사용자의 커서가 링크에서 벗어나면 페이지 미리 로드를 중지하는 등 다른 방식보다 훨씬 효율적이고 방해가 덜 되는 방식으로 프리로딩 동작을 구현합니다. 이것이 바로 제가 Tachyon을 만든 이유이며, 지금까지도 왜 다른 대안이 이 기능을 제공하지 않는지 모르겠습니다. 그 결과, Tachyon은 다른 대안에 비해 사이트에 대역폭 부하를 극히 일부만 추가합니다.
다른 프로젝트보다 기능이 적은 것도 아닙니다(인스턴트 페이지와 가상 기능 동등성 및 몇 가지 추가 기능이 있습니다). 단지 다른 프로젝트보다 간결한 방식으로 구현된 기능일 뿐입니다. 별도의 설정 없이 모바일을 지원하고 화이트리스트, 블랙리스트, 사용자 지정 타이밍 및 동일 출처 제한을 구현하며, 이러한 기능을 사용하기가 훨씬 쉽습니다. 매우 복잡한 기능이 필요한 경우 Tachyon이 최선의 선택이 아닐 수 있지만, 그 외의 모든 사용자에게는 처음부터 최고의 옵션이 될 수 있도록 설계된 Tachyon이 적합합니다.
대용량 리스트에서 selected 아이템 조회 최적화
O(n×m) → O(n+m)로 개선: Map으로 인덱싱
// 기존: 매번 find (느림)
selected.map((key) => items.find((item) => item.key === key))
// 개선: Map 인덱싱 (빠름)
const itemsMap = new Map(items.map((item) => [item.key, item]))
selected.map((key) => itemsMap.get(key)).filter(Boolean)
React에서 백그라운드 인덱싱:
function useItemsIndex(items: Item[]) {
const [map, setMap] = useState(new Map())
const [ready, setReady] = useState(false)
useEffect(() => {
// 청크 단위로 처리하여 UI 블로킹 방지
const newMap = new Map(items.map((item) => [item.key, item]))
setMap(newMap)
setReady(true)
}, [items])
return { map, ready }
}
10만개 이상이면 Web Worker 고려.
WeakMap의 키를 “입력 배열의 참조 자체”로 쓰면 캐시 무효화 로직이 0줄이 된다 — 무효화를 참조 동등성에 위임.
const indexCache = new WeakMap<Item[], Map<string, string>>()
// name → id 역인덱스를 1회 빌드 (O(n))
const buildIndex = (items: Item[]) =>
new Map(items.map((it) => [it.name, it.id]))
function lookup(items: Item[], name: string): string | null {
let index = indexCache.get(items)
if (!index) {
index = buildIndex(items) // 캐시 미스일 때만 빌드
indexCache.set(items, index)
}
return index.get(name) ?? null
}
- 같은 참조로 재호출 → cache hit, O(1)
- 데이터가 새 참조로 교체(refetch 등) → 자동 miss → 새 index 빌드. 옛 index는 옛 items와 함께 GC 대상
- 키가 weak reference라 items가 어디서도 안 잡히면 entry도 같이 사라짐. 일반
Map이면 참조가 영구히 붙들려 메모리 누수
전제: 호출 측 items 참조가 stable해야 작동. 매번 [...data]·data.filter()로 새 배열을 넘기면 항상 miss라 무의미. (TanStack Query의 data/select 결과는 참조 안정성을 보장하므로 그대로 넘기면 OK)
참고
핵심: parser 진행과 first paint는 분리된다. 깃발(first paint)이 보라 사각형(JS 실행)보다 왼쪽이면 — JS가 손대기 전에 그려지면 — 사용자가 잘못된 상태를 본다.
- non-blocking (async/defer): paint가 JS보다 먼저 → flash window(빨간 구간) 발생.
- parser-blocking (동기
<script>): parser는 멈추지만 그 위 청크는 먼저 그려질 수 있어, default 상태가 한 프레임 노출될지가 브라우저 재량 = 보장 없음. - blocking=render (
<head>module): paint가 JS 뒤로 → 유일하게 깨끗.
트레이드오프: “1프레임 flash/CLS”를 막는 대가로 “FCP를 JS 실행시간만큼 미루는” 거래라, 스크립트가 작고 인라인일 때만 성립한다.
성립 조건 셋:
- 스크립트가 작고 인라인 (FCP 지연을 감당할 만큼).
<head>안에서만 동작 — 컴포넌트 마크업 뒤가 아니라 head에type="module" blocking="render"로 올려야 보장이 생긴다 (원문이 놓친 부분).- attribute가 무시돼도 옛 동작으로 떨어질 뿐 깨지지 않는다 → 라이브러리가 권장 패턴으로 제공하려면 이 안전한 폴백이 핵심.