Previously I blogged about many concerns that I have about the sorts of examples that Angular sets (deep in its core libraries) and how I feel like it leads to a âpit of failureâ when it comes to reliability engineering and performance optimization. In my day job I feel backed into a corner where I have to support Angular application development and have had to become something of the performance expert and performance âofficerâ against my early advice that we should have picked anything but Angular. In the end of that complaint post, to offer something constructive, I offered some bon mots about what you might do if you were to rebuild Angular from nearly scratch (I called that idea âProject Gawkyâ if you want to skip to it). At the end of the day though, Iâm a pragmatic software engineer. Itâs always my job to build my way out of problems, especially solving other peopleâs problems.
The short story is that Iâve built a growing collection of libraries around what Iâve called the Pharkas Component Framework. It codifies a lot my âObservables-onlyâ best practices into what I hope is a âpit of successâ tool thatâs easy to slot into existing (âbrownfieldâ) Angular applications and migrate things a component at a time as you can. I think it is incredibly useful out of the box and have been using it to improve performance in production apps for months now. At the very least, I hope these libraries serve as a good example to the Angular ecosystem, whether or not it sees strong adoption outside of production apps that Iâm personally charged to âgrease the wheels ofâ.
I thought I would narrate some of the longer story as well. Iâm very proud of Pharkas and what it accomplishes as an example to get around what I think are problems in Angular deep in the core libraries of the framework, but I also feel like I need to provide at least as much motivation and context as I can on why I built this to help answer why anyone should trust Angular libraries written by someone that unequivocally admits to hating working in Angular.
I Picked a Losing Fight With Zone.js
When I last blogged about Angular I was obviously already trying to think of constructive ways to build my way out of the mess. I mentioned these âProject Gawkyâ ideas in case they sparked someone else to maybe put in the work, because at the time they mostly revolved around replacing or somehow augmenting/extending Angularâs template language compiler. Angular likes to pretend that it doesnât have a template language âit just uses HTMLâ and as you would imagine this means that Angularâs (massive) template compilers (there have been several massive rewrites) themselves are somewhat âsecretiveâ in what of its internals are publicly documented. They arenât really built for easy replacement or augmentation/enhancement. That lack of tools support is the core to why âProject Gawkyâ was much more of a pie-in-the-sky rewrite idea than a pragmatic solution to offer.
Soon after publicly documenting those thoughts on my blog, but while I was still in the middle of thinking about trying to construct my way out Angular, I got tossed into a massive performance fight where the biggest production app I was working on would just âstall outâ for minutes of wall clock time. There was no noticeable network activity, no useful âprogressâ indication, terrible responsiveness to user interactions (âslowâ/âignoredâ clicks), and not even a âplease wait/processingâ beach ball or spinning hour glass: it was just the absolute possible worst user experience and it was making our production users angry.
There wasnât a clear indication of when the problem started, much less if it was a performance regression specific to any recent code. (There wasnât even a clear indication of a specific source/cause. The reproduction was ânavigate the app randomly for long enoughâ.) There was some heavy calculation work in an observable pipeline that recently was refactored just a tiny bit, so in terms of hypotheses, and enough evidence that pipeline was shared by enough components on most pages that was my best idea of a place to start. I started in the obvious places of making sure that the pipeline wasnât over-subscribed, wasnât leaking subscriptions without unsubscribes, and wasnât accidentally over-observing to many input events from other pipelines.
I started with a lot of taps and console.logging debugging, and in the
middle of that was pointed to RxJS-Spy which is a fantastic debugging
tool and I canât recommend enough. It provides a simple tag operator
where you give a pipeline a name, which is a no-op in production
builds but in debug builds gives you an entire dev console framework to
spy on specific pipelines by name or groups of pipelines by regex. It
offers the ability to choose between console.logging and debugger
breakpoints. Again, itâs just a great improvement on âtap-styleâ
debugging. Install it today. (I have nothing to do with RxJS-Spy, I just
keep recommending it to projects now.)
The more I tagged with RxJS-Spy the more I verified that the appâs observable pipelines didnât have obvious leaks and were observing things at a pace that seemed reasonable, including the massive possibly expensive calculations I was worried about in my hypothesis. At this point I had easily disproved my hypothesis.
This is the part of debugging that gives everyone nightmares: all of my teamâs code is working just as expected. Does that mean the performance issue isnât in my teamâs code?
In just about any other framework I would have already have pulled out
âflame graphsâ from the Browserâs performance developer tools and been
trying to base my hypotheses on real, hard evidence, not just shooting
from the hip in the dark or trying to litter the entire code base with
console.logs in the hopes that I could guess at performance
bottlenecks. In Angular it is really hard to get useful data out of
flame graphs for one specific reason: Zone.js.
Zone.js is a supposed âprollyfillâ to implement a JS proposed feature that ECMA Technical Committee 39 (TC-39) shot down years ago for being dangerous, confusing, and not generally useful. So far as Iâm aware, Angular remains the only âcustomerâ of Zone.js, and today is entirely embedded in the Angular repo. Angular uses Zone.js deeply to power its change detection systems. Zone.js works by monkey patching the entire JS world like a virus or other malware: it infects every Event callback, every Promise, and every RxJS Observable. It plugs in a bunch of its own guts in the middle of every bit of code you try to run in an Angular app.
This âinfectionâ is completely visible in any Angular production flame graph. It changes and impacts every single execution stack in the application. Look at the flame graph and the flames are all Zone.js. You may insert here in your mind an âEverything is fineâ meme with the dog labeled Angular and all the flames labeled Zone.js.
Somewhere in those Zone.js flames your code is probably running. Somewhere.
I captured some of these wall clock stalls in the performance tools. I knew to expect most of the flame graphs to be Zone.js nonsense. I assumed with the stalls taking minutes of wall clock time that something not Zone.js should be visible enough in that haystack to make a difference and make it possible to find.
I consistently found no needles in that haystack. I had minutes and minutes of call stacks and the further I dug in the more it was all Zone.js haystack and not a single needle of application code. Was the performance problem entirely Zone.js? I had no good ideas from the glimpses of internal-only Zone.js APIs and source files in the stack traces to tell in any reasonable way whatever it thought it was doing. (I still have no good ideas or answers months later. Zone.js remains a terrifying horror mystery to me.)
At this point in the horror movie (Happy Halloween! I guess you can now guess why I was maybe saving this story for this month; itâs a debuggerâs ghost story) several audience members would be shouting at me: Zone.js has a debugger mode and turning it on is buried as a comment line in the Dev environment.ts file in every template-scaffolded Angular application, because they presume you will need it at some point. As a developer with a lot of experience in debugging, I find deep behavior changes between environments spooky. It was at this point where I felt that I was out of debug options and I needed that frightening last option. I cautiously opened that last door.
With Zone.js in that weird debug mode, I could no longer reproduce the pauses and the application performed better than production. đ» Boo! Itâs haunted! Youâre going to die! Get out of the house! đ»
Zone.js Must Die
This absolutely is one of my deepest nightmares as a sometimes âperformance expertâ: the bad performance is coming from inside the framework itself! The framework acts weirdly different in debug and production environments and itâs the production environment experiencing the worse performance in a way that makes no sense. You canât debug your way out of the production problem because your debugger canât reproduce it.
Hyperbolically, I went insane here. I lost my damn mind.
I had hard evidence that Angular was a horror show under the covers and was causing our production users real pain, anguish, and suffering. But unfortunately, I donât have the power to convince an entire company that the Sunk Cost Fallacy is real and less of a problem than trying to keep sleeping in the haunted horror house because we got such a good deal when we bought it from the previous owners who died of mysterious circumstances that surely were unrelated to why the house was on sale. Iâm told to âjust do my jobâ and patch a fix.
That left to me the only âlogicalâ and âpragmaticâ realization: Zone.js Must Die.
I suppose in the horror film analogy this is the realization by the final girl that Zone.js really is some sort of serial killer and it is time for her to roll up her sleeves and go on the offense and fight back against that ruthless serial killer.
So I started researching everything I could to murder Zone.js without breaking Angular.
Angular is kind enough to give you the option to boot up with a noop âZoneâ and entirely disable Zone.js. Unfortunately, this breaks Angular Change Detection in weird ways and most apps stop functioning at this point if you just switch to the noop âZoneâ.
Angularâs Change Detection apparatus is a direct consequence of Angularâs broken compromises between providing RxJS Observables and then also providing tons of imperative escape hatches. Observables are entirely âpushâ: they push notifications when changes happen. You shouldnât need change detection in a pure Observable world, you already have change notification (âfor freeâ), because that is what Observables are. (This is where the âProject Gawkyâ idea gets most of its promise: with âfreeâ change notifications you can wire it to do some very smart things also âfor freeâ.) But Observables are âhardâ and Angular couldnât commit to them and the resulting worst of both worlds compromise âneedsâ Change Detection.
That Change Detection uses Zone.js to tell it any time anything happens in the app, ever. Zone.js figures this out by wrapping all the Events, Promises, and Observables in the world that it can find with extra instrumentation. Just to tell Angular âhey, something changed somewhere, I donât know, maybeâ (not even really what changed, certainly not to the specific level of individual Observable pushes). Angular still has to do a ton of work after those Zone.js callbacks to figure out what exactly changed and then from there what to update in the templates/DOM.
Fortunately Angular seems to have actually anticipated this, too, that with Observables you have a âpushâ based system for notifications already and in theory shouldnât need change detection at all. It took me something of a deep dive into some of the less well documented parts of Angular, but it turns out the framework indeed has left component developers a âmanual stick shiftâ option for writing components: you can annotate in the Component decorator that the component uses the Change Detection Strategy named âOnPushâ and that you will push all change notifications manually.
The âOnPushâ Change Detection Strategy does give you an offense
strategy to use to fight Zone.js from the bottom-up of an application,
and it needs to be from the bottom up: OnPush components do not need
to be wrapped in Zones (and generally arenât, though Zone.js is
âviralâ in nature and there are no guarantees it doesnât accidentally infect), which is great. But
that also means that OnPush components can only ever use other OnPush
components. Components that use the âDefaultâ change detection
strategy and need Zone.js to detect their changes can use OnPush
components just fine, but not the other way around.
But a âbottom up onlyâ hope in a brownfield application is still a ton of hope to make a noticeable change. A âmanual stick shiftâ option isnât ideal, but that too gives hope that you have something that you can automate and that you can build an automatic transmission on top of a manual stick shift with software. Itâs not pretty, but it is âpragmaticâ and it will get the job done.
Introducing the Pharkas Component Framework
To recap: I lost my mind in horror. I decided that Zone.js must die. Then I finally discovered some hope for a âbottom-up solutionâ.
I realized that I could build it: I could codify my âObservables onlyâ way of building components into a library, and use that library to build a handy âautomatic transmissionâ to replace Zone.js-based Change Detection with something smarter and less compromised (if it sticks to âObservables onlyâ).
Unlike âProject Gawkyâ, I had a firm place to start to build a useful,
reusable library for building (Zone-free) OnPush components in an
Observables only way that could provide not just automated push-based
change detection to Angular, but even bring in some of the âsmartsâ
ideas of âProject Gawkyâ and apply them as good defaults. By making
them good defaults I hope that my library can build not just a âpit of
successâ but a âpit of smart successâ to the developers that choose to
use it. For instance, React took several major versions worth of
revisions and refactoring and a lot of code to deliver âconcurrent
modeâ which deprioritizes most DOM work until after idle callbacks such as
requestAnimationFrame helping the browser to focus on interactivity
over DOM element thrashing. Concurrent mode is still not yet the
default in React for several compatibility reasons and needs to be
opt-in. Iâve built something similar in my own library for debouncing
change notifications to Angular to nice clean requestAnimationFrame time just using Observable schedulers in
very little code (itâs probably a lot more documentation than code at
this point), and it is default and (simple) opt-out. (While it is at
it, the library also takes care of boring Angular administrative
trivia such as ngOnInit and ngOnDestroy lifecycle callbacks.)
Overall, I feel like this library has turned into some of the best
documented and well-tested open source Iâve had the pleasure to work
on. Iâm not entirely satisfied with the testing just yet, as Iâm
waiting for Angular to make the leap to the next major version of RxJS
to get some good âmarble diagramâ timing tests added. Because of that
useful default of debouncing to requestAnimationFrame, I need a marble
diagram harness that understands and fakes requestAnimationFrame
timing, which the next major of RxJS supports out of the box and I
wasnât happy with backports I attempted for the current Angular
supported RxJS.
I named this library âAngular Pharkasâ and the approach the âPharkas Component Frameworkâ. This name is a terrible joke, that is possibly only funny to myself. I had lost my mind, remember, and I needed to scrape out whatever sanity I could out of this entire horror situation, and had to get whatever I built into production ASAP to make users happy (and naming is indeed one of the hardest problems in all of computer science). So I named it a joke and filled its README with a few jokes to amuse myself. Itâs maybe not the most âprofessionalâ approach, but sometimes we need humor in our darkest hours.
To entirely over explain the joke: Freddy Pharkas: Frontier Pharmacist was a 1993 adventure game from Sierra On-Line near the peak of their development golden age. It can be described as the âBlazing Saddles of videogamesâ and is a joke filled satire of cowboy, Western, and Old West tropes in which the title character just wants to be a respectable, civilized Pharmacist selling prescriptions in a lawless frontier town. (I recall it nearly breaks the fourth wall as hard as Blazing Saddles as well, but it has been a decade easily since I last played it. The comparison is not entirely unearned, for those that have a high opinion of Blazing Saddles.) As someone trying to peddle RxJS best practices in a sometimes lawless-feeling ecosystem, I sometimes feel like a frontier pharmacist when working in Angular. (The terrible pun there being that âRxâ in addition to technically meaning âReactive Extensionsâ, which was the original .NET name for its Observables-pattern framework, is also one of the more common abbreviations for the word âprescriptionâ sometimes stylized â and has been used by pharmacists for that word for a long time, from latin ârecipeâ meaning âtakeâ.)
The Growing Pharkas âFamilyâ
Beyond the base component and the library that provides the core âPharkas Component Frameworkâ, Iâve been slowly accumulating a lot of ancillary libraries of other open source components and component base classes that make sense to release next to it.
So far the biggest running theme of these other components is
providing Angular wrappers for âVanilla JSâ components. There are a
number of factors behind this including the sorts of components Iâve
needed to work on for my day jobâs production apps, navigating which
components are âbusiness critical/secret sauce/non-disclosableâ versus
which seem good candidates to open source (or clean room rewrite as
open source in my spare time, because I lost my mind and have done some moonlighting here) because they have no domain specific
code, and that the âbottom upâ approach to converting to OnPush
components especially highlights your âVanillaJS wrapper componentsâ
as a key âbottomâ that needs conversion early.
I think âVanilla JSâ components (and components from outside
frameworks embedded inside Angular) are especially ripe to gain the
benefits of OnPush style components: they shouldnât have any
change detection needs because they handle everything internally.
Wiring all of a âVanilla JSâ componentâs Event handlers, Promises, and
even Observables with Zone.js just because it may in very unlikely
cases result in a change to detect is possibly the purest example of
obviously unnecessary overhead. Zone.js tries not to be that
âviralâ, and most existing Angular wrapper components know the pain of
what that means and all the little things that need to be wrapped in
an NgZone.run callback. (Default components using OnPush
components donât need NgZone.run callbacks in my experience, that
boundary is handled automatically enough, unlike the âVanilla JSâ boundary.)
I think these Pharkas âfamilyâ of âVanilla JSâ wrappers should serve
as useful examples of the gains to be made in using OnPush component
wrappers in all cases. There should be no doubts that the performance
is better in the boundary spaces between Angular and not-Angular.
Thereâs no NgZone injections and no NgZone.run calls. Thereâs no
change detection notifications necessary at all when the component
handles all of its own update cycles.
I think they also serve as good, interesting examples of the types of setup and teardown you can do when you think entirely in Observables. I think thatâs often one of the things developers complain the most that they need imperative âescape hatchesâ from Observables for (and why Angular is the bizarre compromise that it is): dealing with the boundaries between components that understand them and those that have more imperative APIs. You can do a lot with Observables if you put your mind to it.
These libraries are also more documentation than code. Some of them are direct drop-in replacements for well known Angular wrapper libraries and I think youâll find less, easier to understand code than the wrappers that they replace, even before you add in the additional benefits that they simply perform better.
The âdemo siteâ for Pharkas right now is a collection of these âVanilla JSâ components themselves (more than one!) used in a combined âdashboardâ with (fake) real time data. Iâm incredibly biased here, but I have never seen performance that strong at the âVanilla JS boundariesâ anywhere else in the Angular world. The real time is âfakeâ but modeled at speeds and amount of data Iâve seen in actual real time dashboards in Production (in Frameworks that are not Angular). Thereâs definitely no strange and unexpected Zone.js stalls. (The demo site is built in the Angular noop âZoneâ so truly has no Zone.js at all even in accidental fallback.)
All of this is MIT licensed open source, and I encourage everyone to at least dig in and glance at the source and maybe try to learn from it, if nothing else, even if you donât think you need any of these libraries in your own production work.
Aside: Observable âState Managementâ
The âPharkas Component Frameworkâ is agnostic to how you build Observables, it only mandates that you use Observables.
There are a lot of options in Angular, many inspired by the React ecosystemâs Redux (in my opinion without understanding the reasoning behind Redux, but that is a complaint for another blog post) such as NgRx, NgXs, and more. Pharkas doesnât care if you use any or none of them. It works well with them. It works well without them.
In my companyâs production we already have a wild hodge podge of all of the above. In my own development and prioritization Iâve taken a âwithout themâ approach I currently call âlots of small Observablesâ which may be possibly called âAtomic Observablesâ in analogy to the React âcounter-Reduxâ term âAtomicsâ. Today I donât have a library to offer on this pattern. I see it as simply a pattern and an âobvious oneâ at that, so I donât think it needs a library at all. (I might even describe it today as a ânaturalâ pattern of Observable building, implying it is the NgRx/NgXs/et al of the world that is perhaps a bit âunnaturalâ.) It is on my TODO list to eventually try to write some better documentation on that pattern in the hopes it sparks joy in some developers.
At one point I thought Pharkas âneededâ a state management answer or to be a little bit less state management âagnosticâ to be a ârealâ Angular Observable library (thanks NgRx/NgXs et al for that bit of impostor syndrome), but I decided YAGNI (you arenât going to need it) and yeeted it out in early versions and have no regrets having done so.
Should You Use Pharkas?
I probably wouldnât if I were you. You likely have no good reason to trust my claims at face value and doubt my credentials. I probably wouldnât even build this library, much less offer to maintain it if I convinced my day job to avoid Angular like the plague, which I have tried to convince them multiple times. I donât blame you to be skeptical.
Highlighted and self-aware summary of previous sections:
- Zone.js was built by developers in the lovely ivory towers of Google. They were confident enough in that effort that they proposed that to actual standards bodies as a way that all future browsers should work in perpetuity.
- The Pharkas Component Framework was written by an impostor syndrome-filled dark matter âEnterpriseâ developer in a company that is not primarily a software company.
- This developer openly admits in a blog post that the framework came out of a fit of insanity.
- The Pharkas documentation files and even project name are full of unprofessional jokes.
- The Pharkas developer and maintainer has admitted to hating the Angular Framework in blog posts and claims that they will drop maintenance support at first chance, depending on their day jobâs needs. (On the other hand, it is MIT licensed Open Source on Github and easy to fork.)
- Pharkas doesnât provide any sort of âState Managementâ.
I scratched my own itch here. I solved some critical production problems that needed solving ASAP, somehow or another. I did what I had to do. This blog post isnât a plea to use this work. I have made promises that I think you would see clear performance gains and kinder, gentler developer experience if you to do that, but I donât expect you to take my word for it.
What I would like? Please learn from it! Itâs a handy, easy to explore
MIT-licensed Open Source repository. If thereâs one particular
takeaway here: use OnPush components everywhere you can! This truly is
a bottom up initiative. It needs to be âgrass rootsâ in Angular, because it is not the default. Component
developers (especially those wrapping âVanilla JSâ components) should
start leaning OnPush component by their own default choice. I hope no one else
experiences the debugging ghost story đ» I did to the same extent and it
truly is as rare as it seems in Angular that more people donât call
Angular the âhaunted house frameworkâ or worse, but after having experienced
that every library I see in Angular I now evaluate on âdoes it use
OnPush components?â. You donât need Pharkas to build OnPush
components. I think Pharkas makes it very easy to do that, and adds
some nice âautomatic transmissionâ and smarts on top of it, so I would
recommend taking a look using it to build your next components, but
again my plea here is only for using OnPush components, I donât care
how you get there (âmanual stick-shiftâ or not).
I hope I have set a good example here.
Happy Halloween, and good luck if you are an Angular developer. đ