1- import { useCallback , useState } from 'react'
1+ import { useCallback , useEffect , useRef , useState } from 'react'
22
33import MiniSearch from 'minisearch'
44
@@ -49,58 +49,108 @@ type SearchDoc = {
4949
5050const SEARCH_RESULTS_LIMIT = 50
5151
52- export const useSearchEngine = (
53- type : SearchType ,
54- limit = SEARCH_RESULTS_LIMIT
55- ) => {
56- const [ engine ] = useState < Promise < MiniSearch < SearchDoc > > > ( async ( ) => {
57- const miniSearch = new MiniSearch < SearchDoc > ( {
58- fields : [ 'title' , 'heading' , 'text' ] , // fields to index for full-text search
59- storeFields : [ 'title' , 'heading' , 'text' , 'path' ] , // fields to return with search results
60- searchOptions : {
61- boost : { title : 2 } ,
62- fuzzy : 0.2 ,
63- prefix : true ,
64- } ,
65- } )
66-
67- if ( typeof window === 'undefined' ) {
68- return miniSearch
69- }
52+ /**
53+ * Deadline for the idle callback that warms the index, so the build still
54+ * happens promptly on browsers that stay busy after the dialog opens.
55+ */
56+ const ENGINE_WARMUP_TIMEOUT_MS = 500
57+
58+ const loadDocIndex = async ( type : SearchType ) : Promise < DocIndex [ ] > => {
59+ switch ( type ) {
60+ case 'help' :
61+ return ( await import ( '../../../../../.contentlayer/en.json' ) )
62+ . default as unknown as DocIndex [ ]
63+ case 'specs' :
64+ return ( await import ( '../../../../../.contentlayer/specs.en.json' ) )
65+ . default as unknown as DocIndex [ ]
66+ }
67+ }
7068
71- let docIndex : DocIndex [ ]
72- switch ( type ) {
73- case 'help' :
74- docIndex = ( await import ( '../../../../../.contentlayer/en.json' ) )
75- . default as unknown as DocIndex [ ]
76- break
77- case 'specs' :
78- docIndex = ( await import ( '../../../../../.contentlayer/specs.en.json' ) )
79- . default as unknown as DocIndex [ ]
80- break
81- }
69+ const createSearchEngine = async (
70+ type : SearchType
71+ ) : Promise < MiniSearch < SearchDoc > > => {
72+ const miniSearch = new MiniSearch < SearchDoc > ( {
73+ fields : [ 'title' , 'heading' , 'text' ] , // fields to index for full-text search
74+ storeFields : [ 'title' , 'heading' , 'text' , 'path' ] , // fields to return with search results
75+ searchOptions : {
76+ boost : { title : 2 } ,
77+ fuzzy : 0.2 ,
78+ prefix : true ,
79+ } ,
80+ } )
8281
83- const docs : SearchDoc [ ] = [ ]
84- let id = 0
82+ if ( typeof window === 'undefined' ) {
83+ return miniSearch
84+ }
8585
86- for ( const item of docIndex ! ) {
87- for ( const [ heading , texts ] of Object . entries ( item . content ) ) {
88- for ( const text of texts ) {
89- docs . push ( {
90- id : id ++ ,
91- title : item . title ,
92- path : item . path ,
93- heading,
94- text,
95- } )
96- }
86+ const docIndex = await loadDocIndex ( type )
87+
88+ const docs : SearchDoc [ ] = [ ]
89+ let id = 0
90+
91+ for ( const item of docIndex ) {
92+ for ( const [ heading , texts ] of Object . entries ( item . content ) ) {
93+ for ( const text of texts ) {
94+ docs . push ( {
95+ id : id ++ ,
96+ title : item . title ,
97+ path : item . path ,
98+ heading,
99+ text,
100+ } )
97101 }
98102 }
103+ }
99104
100- miniSearch . addAll ( docs )
105+ miniSearch . addAll ( docs )
101106
102- return miniSearch
107+ return miniSearch
108+ }
109+
110+ const whenIdle = ( callback : ( ) => void ) : ( ( ) => void ) => {
111+ if ( typeof window . requestIdleCallback !== 'function' ) {
112+ const timeoutId = window . setTimeout ( callback , 0 )
113+ return ( ) => window . clearTimeout ( timeoutId )
114+ }
115+
116+ const handle = window . requestIdleCallback ( callback , {
117+ timeout : ENGINE_WARMUP_TIMEOUT_MS ,
103118 } )
119+ return ( ) => window . cancelIdleCallback ( handle )
120+ }
121+
122+ type Options = {
123+ /**
124+ * Whether the index may be built. The doc index is ~540KB and indexing it
125+ * blocks the main thread for hundreds of milliseconds on mobile, so callers
126+ * enable it only once the user reaches for search — never on page load.
127+ */
128+ enabled ?: boolean
129+ limit ?: number
130+ }
131+
132+ export const useSearchEngine = ( type : SearchType , options : Options = { } ) => {
133+ const { enabled = true , limit = SEARCH_RESULTS_LIMIT } = options
134+
135+ const engineRef = useRef < Promise < MiniSearch < SearchDoc > > | null > ( null )
136+
137+ const loadEngine = useCallback ( ( ) : Promise < MiniSearch < SearchDoc > > => {
138+ engineRef . current ??= createSearchEngine ( type )
139+
140+ return engineRef . current
141+ } , [ type ] )
142+
143+ // Warm the index once search is reachable, but off the interaction that
144+ // opened it, so building it never delays the dialog's first paint.
145+ useEffect ( ( ) => {
146+ if ( ! enabled ) {
147+ return
148+ }
149+
150+ return whenIdle ( ( ) => {
151+ void loadEngine ( )
152+ } )
153+ } , [ enabled , loadEngine ] )
104154
105155 const [ results , setResults ] = useState < Result [ ] > ( [ ] )
106156
@@ -113,7 +163,7 @@ export const useSearchEngine = (
113163 return
114164 }
115165
116- const searchResults = ( await engine )
166+ const searchResults = ( await loadEngine ( ) )
117167 . search ( normalizedTerm )
118168 . slice ( 0 , limit ) as SearchResult [ ]
119169
@@ -220,7 +270,7 @@ export const useSearchEngine = (
220270
221271 setResults ( results )
222272 } ,
223- [ engine , limit ]
273+ [ loadEngine , limit ]
224274 )
225275
226276 return { results, query } as const
0 commit comments