@@ -127,11 +127,11 @@ <h1>Adaptive Tier Test</h1>
127127 < span class ="info-value " id ="info-motion "> —</ span >
128128 </ div >
129129 < div class ="info-row ">
130- < span class ="info-label "> Measured Speed </ span >
130+ < span class ="info-label "> Probe Round-Trip </ span >
131131 < span class ="info-value " id ="info-speed "> measuring...</ span >
132132 </ div >
133133 < div class ="info-row ">
134- < span class ="info-label "> Probe Latency</ span >
134+ < span class ="info-label "> Probe Latency (raw) </ span >
135135 < span class ="info-value " id ="info-latency "> —</ span >
136136 </ div >
137137 < div class ="info-row ">
@@ -199,110 +199,108 @@ <h2 style="margin-top: 0">How This Works</h2>
199199 </ div >
200200
201201 < script >
202- // ─── Speed Probe: measure actual download speed ───────────────────
203- // Downloads a small resource and measures how long it takes.
204- // This works with DevTools throttling (unlike navigator.connection).
202+ // ─── Dual detection: navigator.connection + latency probe ────────
203+ //
204+ // Strategy:
205+ // 1. navigator.connection gives the network TYPE (4g/3g/2g) — accurate on real devices
206+ // 2. Latency probe measures round-trip time — works with DevTools throttling
207+ // 3. Take the WORSE of the two — handles both real slow connections AND simulated throttling
205208
206- async function measureSpeed ( ) {
209+ async function measureLatency ( ) {
207210 const progress = document . getElementById ( 'probe-progress' )
208211 progress . style . width = '30%'
209-
210212 try {
211- // Use a cache-busted fetch of this page itself as a probe
212- // The page is ~12KB — enough to measure, small enough for GPRS
213- const probeUrl = window . location . href + '?probe=' + Date . now ( )
213+ const url = window . location . href . split ( '?' ) [ 0 ] + '?probe=' + Date . now ( )
214214 const start = performance . now ( )
215- const response = await fetch ( probeUrl , { cache : 'no-store' } )
216- const blob = await response . blob ( )
215+ await fetch ( url , { cache : 'no-store' } )
217216 const elapsed = performance . now ( ) - start
218- const bytes = blob . size
219- const bitsPerSecond = ( bytes * 8 ) / ( elapsed / 1000 )
220- const mbps = bitsPerSecond / 1_000_000
221-
222217 progress . style . width = '100%'
223-
224- return {
225- mbps : Math . round ( mbps * 100 ) / 100 ,
226- latencyMs : Math . round ( elapsed ) ,
227- bytes : bytes ,
228- method : 'speed-probe'
229- }
230- } catch ( e ) {
218+ return Math . round ( elapsed )
219+ } catch {
231220 progress . style . width = '100%'
232- return { mbps : - 1 , latencyMs : - 1 , bytes : 0 , method : 'probe-failed' }
221+ return - 1
233222 }
234223 }
235224
236- // ─── Tier Detection ──────────────────────────────────────────────
225+ function tierFromLatency ( ms ) {
226+ if ( ms < 300 ) return { tier : 'premium' , motion : 3 , reason : 'Fast probe: ' + ms + 'ms' }
227+ if ( ms < 1000 ) return { tier : 'standard' , motion : 2 , reason : 'Moderate probe: ' + ms + 'ms' }
228+ if ( ms < 3000 ) return { tier : 'standard' , motion : 1 , reason : 'Slow probe: ' + ms + 'ms' }
229+ return { tier : 'lite' , motion : 0 , reason : 'Very slow probe: ' + ms + 'ms' }
230+ }
231+
232+ function tierFromConnection ( conn ) {
233+ if ( conn . saveData ) return { tier : 'lite' , motion : 0 , reason : 'Save-Data enabled' }
234+ var type = conn . effectiveType
235+ var dl = conn . downlink || 10
236+ var rtt = conn . rtt || 50
237+ if ( type === 'slow-2g' || type === '2g' ) return { tier : 'lite' , motion : 0 , reason : 'Connection: ' + type }
238+ if ( type === '3g' && dl < 0.5 ) return { tier : 'standard' , motion : 1 , reason : 'Slow 3G: ' + dl + 'Mbps' }
239+ if ( type === '3g' ) return { tier : 'standard' , motion : 2 , reason : '3G: ' + dl + 'Mbps' }
240+ if ( dl >= 1 || rtt < 100 ) return { tier : 'premium' , motion : 3 , reason : 'Fast: ' + dl + 'Mbps / ' + rtt + 'ms' }
241+ if ( dl >= 0.4 ) return { tier : 'standard' , motion : 2 , reason : 'Moderate: ' + dl + 'Mbps' }
242+ return { tier : 'standard' , motion : 1 , reason : 'Slow 4G: ' + dl + 'Mbps' }
243+ }
237244
238245 async function detect ( ) {
239- const badge = document . getElementById ( 'tier-badge' )
240- badge . textContent = '⏳ Measuring speed ...'
246+ var badge = document . getElementById ( 'tier-badge' )
247+ badge . textContent = '⏳ Detecting ...'
241248 badge . setAttribute ( 'data-tier' , 'standard' )
242249
243- const conn = navigator . connection || navigator . mozConnection || navigator . webkitConnection
250+ var conn = navigator . connection || navigator . mozConnection || navigator . webkitConnection
244251
245- // Quick checks first (no network needed)
252+ // Quick OS-level checks
246253 if ( window . matchMedia && window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ) {
247- return applyResult ( 'lite' , 0 , 'prefers-reduced-motion' , 'os-preference' , null , conn )
254+ return applyResult ( 'lite' , 0 , 'prefers-reduced-motion' , 'os-preference' , - 1 , conn )
248255 }
249256 if ( conn && conn . saveData ) {
250- return applyResult ( 'lite' , 0 , 'Save-Data enabled' , 'save-data' , null , conn )
257+ return applyResult ( 'lite' , 0 , 'Save-Data enabled' , 'save-data' , - 1 , conn )
251258 }
252259
253- // Measure actual speed
254- const speed = await measureSpeed ( )
260+ // Get both signals
261+ var connResult = conn && conn . effectiveType ? tierFromConnection ( conn ) : null
262+ var latencyMs = await measureLatency ( )
263+ var probeResult = latencyMs >= 0 ? tierFromLatency ( latencyMs ) : null
255264
256- if ( speed . mbps < 0 ) {
257- // Probe failed — fall back to navigator.connection
258- if ( conn && conn . effectiveType ) {
259- const type = conn . effectiveType
260- if ( type === 'slow-2g' || type === '2g' ) return applyResult ( 'lite' , 0 , 'Connection API: ' + type , 'connection-api' , speed , conn )
261- if ( type === '3g' ) return applyResult ( 'standard' , 2 , 'Connection API: 3G' , 'connection-api' , speed , conn )
262- return applyResult ( 'premium' , 3 , 'Connection API: ' + type , 'connection-api' , speed , conn )
263- }
264- return applyResult ( 'standard' , 2 , 'Could not measure speed' , 'fallback' , speed , conn )
265- }
265+ var tier , motion , reason , method
266266
267- // Classify based on ACTUAL measured speed
268- let tier , motion , reason
269- if ( speed . mbps >= 2 ) {
270- tier = 'premium' ; motion = 3
271- reason = 'Fast: ' + speed . mbps + ' Mbps'
272- } else if ( speed . mbps >= 0.5 ) {
273- tier = 'standard' ; motion = 2
274- reason = 'Moderate: ' + speed . mbps + ' Mbps'
275- } else if ( speed . mbps >= 0.1 ) {
276- tier = 'standard' ; motion = 1
277- reason = 'Slow: ' + speed . mbps + ' Mbps'
267+ if ( connResult && probeResult ) {
268+ // Take the WORSE of the two signals
269+ if ( probeResult . motion <= connResult . motion ) {
270+ tier = probeResult . tier ; motion = probeResult . motion
271+ reason = probeResult . reason + ' | API: ' + connResult . reason
272+ method = 'probe (downgraded from API)'
273+ } else {
274+ tier = connResult . tier ; motion = connResult . motion
275+ reason = connResult . reason + ' (probe: ' + latencyMs + 'ms)'
276+ method = 'connection-api (confirmed by probe)'
277+ }
278+ } else if ( probeResult ) {
279+ tier = probeResult . tier ; motion = probeResult . motion
280+ reason = probeResult . reason ; method = 'probe-only'
281+ } else if ( connResult ) {
282+ tier = connResult . tier ; motion = connResult . motion
283+ reason = connResult . reason ; method = 'connection-api-only'
278284 } else {
279- tier = 'lite' ; motion = 0
280- reason = 'Very slow: ' + speed . mbps + ' Mbps'
285+ tier = 'standard' ; motion = 2 ; reason = 'No signals' ; method = 'fallback'
281286 }
282287
283- // Cross-reference with connection API if available
284- if ( conn && ( conn . effectiveType === 'slow-2g' || conn . effectiveType === '2g' ) ) {
285- tier = 'lite' ; motion = 0
286- reason += ' (confirmed by connection API: ' + conn . effectiveType + ')'
287- }
288-
289- applyResult ( tier , motion , reason , speed . method , speed , conn )
288+ applyResult ( tier , motion , reason , method , latencyMs , conn )
290289 }
291290
292- function applyResult ( tier , motion , reason , method , speed , conn ) {
291+ function applyResult ( tier , motion , reason , method , latencyMs , conn ) {
293292 setTier ( tier )
294-
295293 document . getElementById ( 'info-tier' ) . textContent = tier
296294 document . getElementById ( 'info-motion' ) . textContent = motion
297- document . getElementById ( 'info-speed' ) . textContent = speed ? ( speed . mbps >= 0 ? speed . mbps + ' Mbps' : 'failed' ) : 'skipped'
298- document . getElementById ( 'info-latency' ) . textContent = speed ? ( speed . latencyMs >= 0 ? speed . latencyMs + ' ms' : '—' ) : '—'
295+ document . getElementById ( 'info-speed' ) . textContent = latencyMs >= 0 ? latencyMs + 'ms round-trip' : 'skipped'
296+ document . getElementById ( 'info-latency' ) . textContent = latencyMs >= 0 ? latencyMs + ' ms' : '—'
299297 document . getElementById ( 'info-conn' ) . textContent = conn ? ( conn . effectiveType + ' / ' + ( conn . downlink || '?' ) + ' Mbps / ' + ( conn . rtt || '?' ) + 'ms RTT' ) : 'unavailable'
300298 document . getElementById ( 'info-savedata' ) . textContent = conn ? ( conn . saveData ? 'Yes' : 'No' ) : '—'
301299 document . getElementById ( 'info-reason' ) . textContent = reason
302300 document . getElementById ( 'info-method' ) . textContent = method
303301
304- const badge = document . getElementById ( 'tier-badge' )
305- const icon = tier === 'premium' ? '✨' : tier === 'standard' ? '⚡' : '🪶'
302+ var badge = document . getElementById ( 'tier-badge' )
303+ var icon = tier === 'premium' ? '✨' : tier === 'standard' ? '⚡' : '🪶'
306304 badge . textContent = icon + ' ' + tier . toUpperCase ( )
307305 badge . setAttribute ( 'data-tier' , tier )
308306 }
@@ -311,7 +309,6 @@ <h2 style="margin-top: 0">How This Works</h2>
311309 document . getElementById ( 'app' ) . setAttribute ( 'data-tier' , tier )
312310 }
313311
314- // Run on load
315312 detect ( )
316313 document . getElementById ( 'timestamp' ) . textContent = 'Page loaded: ' + new Date ( ) . toLocaleTimeString ( )
317314 </ script >
0 commit comments