three.js 로 만든 화면에서 첫 클릭 때만 렉이 걸린다. 두 번째부터는 멀쩡하다. 또는 RAG(검색 증강 생성) 챗봇의 응답이 매 턴 4~5초씩 걸리고, 로그를 보면 지식 검색이 자주 시간 초과로 비어 있다. 둘은 다른 문제처럼 보이지만 원인이 같다. 사용자가 기다리는 바로 그 순간에, 기다릴 필요가 없던 준비 작업이 끼어 있다.
지연은 어디서 오나
사용자가 기다리는 경로 안에서 하는 일은 전부 그대로 지연이 되므로, 그 순간에 꼭 필요하지 않은 준비 — 처음 한 번 드는 비용이든 매번 드는 외부 호출이든 — 는 그 순간 전으로 옮겨야 한다. 코드를 빠르게 하는 것보다 일을 놓는 자리를 바꾸는 쪽이 대개 효과가 크다. 같은 일을 같은 양만큼 하되, 사용자가 화면을 보며 기다리지 않는 시간에 한다.

첫 클릭 렉은 왜 첫 번째에만 생기나
three.js 에서 "처음 한 번만 걸리는" 상호작용 렉은 대개 셰이더 컴파일이다. 클릭에 반응해 새 메시와 새 재질을 만들면, 그 재질이 처음 그려지는 프레임에 GPU 프로그램이 컴파일된다. 두 번째부터는 컴파일된 프로그램을 다시 쓰니 빠르다.
고치는 법은 그 컴파일을 로딩 쪽으로 당기는 것이다.
- 상호작용 때
new Mesh/new Material을 하지 않는다. 쓸 것을 시작할 때 미리 만들어 둔다(풀). - 미리 만든 것은 장면에 넣어 한 번 그려지게 하고, 보이지 않게 숨긴다(투명도 0 등). 이 첫 그림에서 컴파일이 일어난다.
- 상호작용 때는 풀에서 꺼내 보이게만 한다.
WebGLRenderer.compile() 로 미리 컴파일하는 길도 있다. 다만 장면 구성에 따라 경고가 여럿 붙을 수 있어서, 실제로 한 번 그려 두는 풀링이 더 깔끔할 때가 많다.
대화 턴마다 하는 검색은 어디로 옮기나
챗봇이 매 턴 답을 만들기 전에 임베딩 검색으로 지식을 끌어오면, 그 검색 시간이 그대로 응답 시간에 더해진다. 응답을 늦추지 않으려고 타임아웃을 짧게 걸면, 이번엔 검색이 자주 시간 초과로 비어 답의 재료가 없어진다. 어느 쪽이든 손해다.
검색을 답을 보낸 뒤로 옮긴다.
- 이번 턴의 답을 먼저 보낸다.
- 답을 보낸 직후, 지금까지의 대화로 다음 턴에 쓸 지식을 비동기로 검색해 저장해 둔다.
- 다음 턴에는 저장된 결과를 꺼내 쓴다.
대가는 한 턴 늦은 맥락이라는 점이다. 사용자의 바로 이번 말은 검색에 안 들어간다. 대부분의 대화에서 지식은 생각의 재료이지 이번 문장의 정답이 아니므로 이 정도면 충분하다. 이번 말에 꼭 맞춰야 하는 검색(주문 번호 조회 같은)은 이 방식에 맞지 않는다.
그리고 실패 로그에는 실패 종류를 남긴다 — 시간 초과인지, 키 오류인지, 응답 형식 오류인지. 종류 없이 "검색 실패"만 남기면 원인을 찾을 수 없다.
어떤 일을 경로 밖으로 뺄 수 있나
이 질문 셋으로 가른다.
- 사용자의 이번 입력이 있어야만 할 수 있는 일인가? 아니면 미리 할 수 있다(컴파일, 리소스 로딩, 연결 열기).
- 한 박자 늦은 입력으로 해도 결과가 쓸 만한가? 그렇다면 앞 턴 뒤로 옮길 수 있다(지식 검색, 추천 후보 계산).
- 처음 한 번만 비싼가, 매번 비싼가? 처음 한 번이면 시작 때로(풀링·워밍업), 매번이면 앞 단계 뒤로(비동기 선행).
에이전트에게 성능 문제를 맡긴다면 지침에 이렇게 적는다.
지연을 줄일 때 코드부터 최적화하지 않는다. 먼저 사용자가 기다리는 경로(입력 → 응답) 안의 작업 목록을 적고,
각 작업에 "이번 입력이 꼭 필요한가"를 묻는다. 필요 없는 작업은 시작 시점이나 앞 응답 뒤로 옮긴다.
"처음 한 번만" 느린 것은 그 순간 처음 쓰이는 자원(셰이더·재질·연결)의 초기화를 의심한다.
실패 로그에는 실패 종류(시간 초과·인증·응답 오류)를 남긴다.
우리가 겪은 일
- WebGL 데모의 첫 클릭 렉. 클릭 때 새 재질을 만들던 코드에서 첫 클릭이 34ms, 이후 1ms 였다(브라우저 프레임 시간 측정). 메시와 재질을 시작 때 풀로 만들어 투명하게 숨겨 두자 첫 클릭도 다른 클릭과 같아졌다.
- 상담형 대화 앱의 응답 지연. 턴마다 동기로 부르던 임베딩 검색이 0.7
1.9초를 먹었고 다섯 번 중 두 번은 시간 초과로 비었다. 답을 보낸 뒤 비동기로 미리 검색하게 바꾸자 턴당 검색 시간이 0.09초, 전체 응답이 3.84.9초에서 2.1~2.3초가 됐다(서버 로그의 단계별 시간 측정).