‹ All essays

Architecture

From Main Thread Blocking to Sub-16ms TTI: The Production Script Optimization Playbook for ServiceNow Architects

The Scenario

You've just inherited a ServiceNow instance where Incident form loads spike to 8+ seconds on Chrome DevTools Performance tab. The culprit: Catalog Client Scripts with 500ms+ Long Tasks from GlideAjax calls in onLoad that query 10k+ record CMDB tables without indexes. Users report 'frozen browser' tickets from field teams, and your SRE dashboard shows 40% TBT regression after the last PA release. As the architect, you need the systematic playbook that turns this into sub-16ms TTI without sacrificing business logic.

Key Questions Answered

Q: What's the root cause diagnosis approach?

A: Open Chrome DevTools Performance tab, record 3x form loads, filter for Long Tasks (>50ms). Pivot to Flame Chart: red blocks in "Scripting" track point to exact line numbers in bundled client scripts. Cross-reference with ServiceNow's sysevent logs filtered by 'glide.ajax' and user session. Production metric: 72% of freezes trace to unindexed GlideRecord.getRowCount() in onChange handlers—your Kubernetes liveness probe equivalent but for script execution.

Q: What are the key concepts I need to master?

A: Query optimization: Replace GlideAjax row counts with scoped REST APIs using encoded queries with indexed fields (sys_id, state). Index strategy: sys_db_object indexes on high-cardinality fields like assignment_group vs low-value sys_updated_on scans. Script efficiency: Batch DOM reads/writes, debounce onChange (your RxJS throttle knowledge applies). Caching: sessionStorage for 60s TTL on CMDB lookups, GlideSession cache for user prefs. Async patterns: Promise.allSettled() over nested GlideAjax, Web Worker polyfill for >100ms computes via postMessage to ServiceNow's sandboxed iframes.

Q: What's the step-by-step solution pattern?

A: 1) Baseline: Performance tab + syslog query='glide.ajax' to quantify Long Tasks. 2) Memoize queries: sessionCache.getOrSet(key, () => new GlideAjax()). 3) Debounce handlers: 250)}. 4) Batch DOM: collect updates in array, g_form.setValue() once per cycle. 5) Async chunk: Promise.all([loadGroups(), loadPriorities()]). 6) Validate: TBT <16ms, LCP <2.5s on throttled 4x CPU.

Q: What are the common mistakes and anti-patterns?

A: Synchronous GlideRecord in onLoad (blocks render like uncached Kubernetes secrets). Nested GlideAjax without timeout=5000ms (waterfall vs your Istio parallel requests). g_form.getReference() without .then() (sync fallback kills main thread). Infinite debounces from unmounted components. Forgetting GlideAjax's getXMLWait()—your GraphQL overfetch equivalent.

Q: How do I prevent this in future development?

A: Enforce eslint-plugin-servicenow with 'no-sync-glideajax' rule. Gate PRs on Lighthouse CI scores >90 mobile. Widget template: