@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 +5 -4
- package/package.json +1 -1
- package/server/analysis/findings.js +8 -4
- package/server/probe.browser.js +114 -1
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
|
|
12
|
-
|
|
13
|
-
generic profile still pinpoints the slow files and
|
|
14
|
-
project — open source, non-profit, no
|
|
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.
|
|
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 === '
|
|
245
|
-
? '
|
|
246
|
-
:
|
|
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
|
-
:
|
|
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)
|
package/server/probe.browser.js
CHANGED
|
@@ -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 (
|
|
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
|
})();
|