liquid-glass-react
Apple이 WWDC25에서 공개한 Liquid Glass 디자인을 React 컴포넌트로 구현한 오픈소스 라이브러리다. npm에서 월간 45,000+ 다운로드를 기록하고 있다.
bashnpm install liquid-glass-react
“
npm · Live Demo · GitHub
기본 사용법
tsximport LiquidGlass from "liquid-glass-react";
function App() {
return (
<LiquidGlass
displacementScale={70}
blurAmount={0.0625}
saturation={140}
aberrationIntensity={2}
elasticity={0.15}
cornerRadius={24}
>
<h2>Your content here</h2>
</LiquidGlass>
);
}
4가지 굴절 모드를 지원한다: standard, polar, prominent, shader.
왜 최적화가 필요했나
원래 버전에서 마우스를 움직이면 초당 60회 이상의 React 리렌더링이 발생했다. 글래스 컴포넌트가 여러 개 있으면 프레임 드랍이 심각했다. 프로파일러로 확인해보니 주요 병목이 명확했다:
- 마우스 좌표를
useState로 관리 → 매 움직임마다 리렌더
getBoundingClientRect()를 프레임당 4-5회 호출
transition: all로 모든 프로퍼티에 전환 효과
- 브라우저 감지를 매 렌더마다 실행
최적화 전략
1. useState → useRef
가장 큰 임팩트. 마우스 좌표를 useRef로 바꾸고 requestAnimationFrame으로 DOM을 직접 업데이트한다.
typescript// Before: 매 움직임마다 리렌더
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
onMouseMove = (e) => {
setMousePos({ x: e.clientX, y: e.clientY }); // 60fps 리렌더!
};
// After: React를 우회한 직접 DOM 조작
const mousePosRef = useRef({ x: 0, y: 0 });
const rafRef = useRef<number>();
onMouseMove = (e) => {
mousePosRef.current = { x: e.clientX, y: e.clientY };
if (!rafRef.current) {
rafRef.current = requestAnimationFrame(() => {
updateDOM(); // 직접 DOM 업데이트
rafRef.current = undefined;
});
}
};
이것만으로 리렌더가 60fps → 0fps로 줄었다.
2. getBoundingClientRect 호출 최소화
프레임마다 4-5번 호출되던 것을 1번으로 통합하고, 결과를 캐시한다.
typescript// rect를 한 번만 계산하고 재사용
const rect = elementRef.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
추가로 ResizeObserver를 사용해 크기 변경 시에만 rect를 재계산한다.
3. GPU 레이어 최적화
css/* transition: all → 특정 프로퍼티만 */
transition: transform 0.3s ease, opacity 0.2s ease;
/* GPU 레이어 프로모션 */
will-change: transform;
/* backdrop-filter 레이어 격리 */
contain: strict;
4. Shader Map 캐싱
shader 모드에서 displacement map을 매번 생성하지 않고 캐시한다.
typescriptconst shaderMapCache = new Map<string, string>();
const getShaderMap = (width: number, height: number) => {
const key = `${width}x${height}`;
if (shaderMapCache.has(key)) return shaderMapCache.get(key)!;
const url = generateShaderMap(width, height);
shaderMapCache.set(key, url);
return url;
};
5. 거리 기반 Early Return
마우스가 글래스 요소에서 멀리 떨어져 있으면 업데이트를 건너뛴다.
typescriptconst updateDOM = () => {
const dx = mousePosRef.current.x - cachedRect.centerX;
const dy = mousePosRef.current.y - cachedRect.centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
// 요소 크기의 3배 이상 떨어져있으면 스킵
if (distance > cachedRect.diagonal * 3) return;
// ... 실제 업데이트
};
벤치마크 결과
6개 페이즈로 나눠서 Original vs Optimized를 비교했다:
| Phase | 설명 | Original FPS | Optimized FPS |
|---|
| Baseline | 기본 렌더링 | 58 | 60 |
| Spawn Storm | 컴포넌트 대량 생성 | 31 | 55 |
| BG Chaos | 배경 애니메이션 | 42 | 58 |
| Mouse Tornado | 빠른 마우스 이동 | 28 | 56 |
| Content Churn | 내부 콘텐츠 변경 | 35 | 57 |
| Combined | 모든 조건 동시 | 18 | 49 |
Combined 페이즈에서 18fps → 49fps, 약 2.7배 성능 향상.
핵심 교훈
- React 리렌더링이 곧 비용이다 — 마우스/스크롤 같은 고빈도 이벤트는
useRef + rAF로 React를 우회하자
- 측정이 먼저다 — 프로파일러 없이 감으로 최적화하면 엉뚱한 곳에 시간을 쓴다
- interleaved 벤치마크 — A를 다 돌리고 B를 돌리면 열 스로틀링으로 편향이 생긴다. 페이즈마다 A/B를 번갈아 돌려야 공정하다
- CSS contain과 will-change — 작지만 누적 효과가 크다
“
소스코드와 벤치마크는 GitHub에서 볼 수 있다.