@progen-labs/spectra 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,10 +8,11 @@ command and it starts your dev server, auto-discovers the actionable elements on
8
8
  of **golden-path journeys**, records each one, and reports a ranked list of culprits — each with the
9
9
  exact function, file and line, the evidence, and a suggested fix.
10
10
 
11
- Deep component-level attribution for **React, Next.js and Vue** (component names, source files, render
12
- counts and per-component render time). For **Svelte, Angular and any other build**, a source-mapped
13
- generic profile still pinpoints the slow files and functions. A [Progen Labs](https://github.com/progen-labs)
14
- project — open source, non-profit, no telemetry, no account required.
11
+ Deep component-level attribution for **React, Next.js, Vue and Angular** (component names, render counts
12
+ and per-component render/change-detection time; source files too, where the framework exposes them). For
13
+ **Svelte and any other build**, a source-mapped generic profile still pinpoints the slow files and
14
+ functions. A [Progen Labs](https://github.com/progen-labs) project — open source, non-profit, no
15
+ telemetry, no account required.
15
16
 
16
17
  ## Install
17
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@progen-labs/spectra",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Spectra — a Chrome DevTools Protocol profiler that identifies the files, functions and components causing frontend slowness, across React, Vue, Svelte, Angular and Next.js builds.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -241,13 +241,17 @@ export function buildFindings(report) {
241
241
  const slowFix =
242
242
  fw === 'vue'
243
243
  ? 'Move expensive work into a computed property, use v-memo/v-once for static subtrees, and virtualise long lists.'
244
- : fw === 'react' || fw === 'next'
245
- ? 'Move expensive calculations into useMemo, virtualise long lists, and avoid deriving large data structures inside render.'
246
- : 'Memoize expensive work, virtualise long lists, and avoid building large data structures during render.';
244
+ : fw === 'angular'
245
+ ? 'Avoid heavy work in template expressions and getters — move it to a memoized value or pure pipe — and use ChangeDetectionStrategy.OnPush.'
246
+ : fw === 'react' || fw === 'next'
247
+ ? 'Move expensive calculations into useMemo, virtualise long lists, and avoid deriving large data structures inside render.'
248
+ : 'Memoize expensive work, virtualise long lists, and avoid building large data structures during render.';
247
249
  const frequentFix =
248
250
  fw === 'vue'
249
251
  ? 'Find the reactive dependency that changes this often (a ref/store/timer), narrow it, and use v-memo to skip unchanged subtrees.'
250
- : 'Find what updates this often (a timer, a store subscription, a scroll handler) and throttle it or narrow the selector/subscription.';
252
+ : fw === 'angular'
253
+ ? 'Reduce change-detection triggers: use OnPush, run timers/listeners with NgZone.runOutsideAngular, and avoid method/getter calls in templates.'
254
+ : 'Find what updates this often (a timer, a store subscription, a scroll handler) and throttle it or narrow the selector/subscription.';
251
255
  if (react.detected && react.hasTimings) {
252
256
  react.components
253
257
  .filter(c => c.wasted >= 5 && c.wasted / c.renders >= 0.3)
@@ -721,6 +721,118 @@
721
721
  probe.frameworkName = 'vue';
722
722
  }
723
723
 
724
+ // ------------------------------------------------ Angular adapter
725
+ // Uses Angular's dev-mode framework profiler (window.ng.ɵsetProfiler, present
726
+ // when ngDevMode is on). Component change-detection fires paired start/end
727
+ // events (2/3) carrying the component instance; a nesting stack turns those
728
+ // into per-component render counts and self time (dur minus children), just
729
+ // like React/Vue. The ng global appears only after bootstrap, so we poll.
730
+ function installAngularAdapter() {
731
+ var ngIds = new Map();
732
+ var nextId = 100000; // keep clear of other adapters' id space
733
+ var stack = [];
734
+
735
+ function clean(n) {
736
+ return (n || '').replace(/^_+/, '');
737
+ }
738
+ function isComponent(instance) {
739
+ try {
740
+ var ctor = instance && instance.constructor;
741
+ if (!ctor) return false;
742
+ return !!ctor['ɵcmp'] || /Component$/.test(ctor.name || '');
743
+ } catch (e) {
744
+ return false;
745
+ }
746
+ }
747
+ function getStat(ctor, name) {
748
+ var id = ngIds.get(ctor);
749
+ if (id === undefined) {
750
+ id = nextId++;
751
+ ngIds.set(ctor, id);
752
+ }
753
+ var stat = probe.stats[id];
754
+ if (!stat) {
755
+ stat = probe.stats[id] = {
756
+ id: id,
757
+ name: name,
758
+ kind: 'angular',
759
+ renders: 0,
760
+ mounts: 0,
761
+ wasted: 0,
762
+ selfMs: 0,
763
+ totalMs: 0,
764
+ maxMs: 0,
765
+ reasons: { props: 0, state: 0, hooks: 0, context: 0, parent: 0 },
766
+ props: {},
767
+ parents: {},
768
+ file: '',
769
+ };
770
+ }
771
+ return stat;
772
+ }
773
+ function onEvent(event, instance) {
774
+ if (!probe.active) return;
775
+ if (event === 2) {
776
+ // Component change-detection start. Push a frame (null for non-component
777
+ // instances like structural-directive contexts) to keep the stack balanced.
778
+ if (!isComponent(instance)) {
779
+ stack.push(null);
780
+ return;
781
+ }
782
+ if (stack.length === 0) probe.commitCount = (probe.commitCount || 0) + 1;
783
+ stack.push({
784
+ ctor: instance.constructor,
785
+ name: clean(instance.constructor.name),
786
+ start: performance.now(),
787
+ child: 0,
788
+ });
789
+ } else if (event === 3) {
790
+ var f = stack.pop();
791
+ if (!f) return;
792
+ var dur = performance.now() - f.start;
793
+ var self = dur - f.child;
794
+ if (self < 0) self = 0;
795
+ for (var i = stack.length - 1; i >= 0; i--) {
796
+ if (stack[i]) {
797
+ stack[i].child += dur;
798
+ break;
799
+ }
800
+ }
801
+ var stat = getStat(f.ctor, f.name);
802
+ stat.renders++;
803
+ stat.selfMs += self;
804
+ stat.totalMs += dur;
805
+ if (self > stat.maxMs) stat.maxMs = self;
806
+ probe.sawTimings = true;
807
+ }
808
+ }
809
+ function register() {
810
+ try {
811
+ var ng = window.ng;
812
+ if (ng && typeof ng['ɵsetProfiler'] === 'function') {
813
+ ng['ɵsetProfiler'](function (event, instance) {
814
+ try {
815
+ onEvent(event, instance);
816
+ } catch (e) {}
817
+ });
818
+ probe.frameworkName = 'angular';
819
+ probe.frameworkDetected = true;
820
+ probe.frameworkVersion =
821
+ (window.ng && window.ng.ɵgetInjectorMetadata ? '' : '') || probe.frameworkVersion;
822
+ return true;
823
+ }
824
+ } catch (e) {}
825
+ return false;
826
+ }
827
+ if (!register()) {
828
+ var tries = 0;
829
+ var iv = setInterval(function () {
830
+ tries++;
831
+ if (register() || tries > 200) clearInterval(iv); // poll up to ~8s
832
+ }, 40);
833
+ }
834
+ }
835
+
724
836
  // ------------------------------------------------ Framework fallbacks
725
837
  // Provide lightweight probes for Vue, Svelte and Angular when React is not detected.
726
838
  function installFrameworkProbes() {
@@ -822,7 +934,8 @@
822
934
  try {
823
935
  var fwHint = SPECTRA_FRAMEWORK && SPECTRA_FRAMEWORK.framework;
824
936
  if (fwHint === 'vue') installVueAdapter();
825
- else if (!WANT_REACT) installFrameworkProbes(); // svelte/angular: heuristic for now
937
+ else if (fwHint === 'angular') installAngularAdapter();
938
+ else if (!WANT_REACT) installFrameworkProbes(); // svelte: heuristic for now
826
939
  } catch (e) {}
827
940
  installObservers();
828
941
  })();