Description
Trading Application with embedded Order Book with responsive design.
Demo
Check it out: https://trading-platform.vercel.app/
- Version with debug logs: https://trading-platform.vercel.app/?debug
- Version with fps metrics: https://trading-platform.vercel.app/?fps
- Version with fps metrics and animation to force frame redraws: https://trading-platform.vercel.app/?fpsAni
Quickstart
yarn
Install dependencies
Local development:
yarn start
Start dev server with hot reloadingyarn sanity
Run all important checks to validate your changes (test/lint/e2e/build)
Production:
yarn build
Build production versionyarn start:prod
Start production server
E2E and Integration tests:
yarn e2e
Builds default project and runs headless cypress e2e tests
Unit tests:
yarn test
Runs all unit tests from projects and libraries.
Structure
Breakdown:
-
Apps – Applications which consume libraries to produce ‘end’ product like Trading App variant with embedded modules.
Can import and embed modules. Either by static or dynamic imports (module federation). Apps can only implicitly include other apps (cypress e2e is one example). Apps cannot be imported by other parts. Apps can import all other parts. Apps also include e2e testing which is core testing flow used in this app. -
Modules – Modules are encapsulated functionality which can be embedded in the apps (implicitly or explicitly). In the future we could support webpack federation and each module would be build separately.
-
Model – Data modeling. Can be imported by other models or modules.
-
Data – Data service layer. Responsible for data retrieval/sending for various communication methods. One example is Streaming data layer using WebSocket.
-
Utils – Small reusable chunk of code not falling into other concepts. Small utility/helper functions. Should not import from any other libs. Can be used by all of them.
-
UI – Contains library of ui components. Follows Atomic Design pattern.
Eslint rules ensure that invalid imports are not possible.
Check @nrwl/nx/enforce-module-boundaries
inside .eslintrc.json
to learn more about constraints.
Testing
E2E and Integration tests
Majority of testing is done in cypress with mocked WebSocked server for various scenarios.
This allows for extensive coverage combined with ease of creating tests with available live preview.
Tests are included as part of separate app called app/trading-platform-e2e which implicitly depends on trading-platform app.
Unit tests
Specific functions are covered using unit tests written in jest and using testing-library as support.
Styled components are tested using snapshotting. Not ideal in all scenarios, but for styled components works great as we need to
focus on generated css definitions, which are small and easy to compare.
Performance
For CPU/Memory usage, used built it devtools monitoring tool. No issues found for prolonged app usage.
Scale of repaints was taken into consideration. Using width change on row caused smaller repaint areas then transform/gradient/background-size solutions.
Amount of layouts changes in all solutions was similar.
Performance testing was done on various devices listed below.
To get stable FPS values, I’ve built FPS tool which also forces each frame to be redrawn.
Without it, browsers can allocate frames as they “please” end do not try to run at constant 60fps.
In addition to performance tests using FPS monitoring, I’ve also validated cpu measurement for reducers and react ‘jobs’ using Performance DevTools.
Main cost post reducing and react handling is layout and painting.
Ways of trying to limit layout cost using css gradients, css background size fills or transforms, didn’t produce any value and increased repaints to whole grid.
Another analysis done was checking impact of key for rows being defined as index vs price. Price as key, produced worse results for order book scenario due to amount of changes in a grid and simple rows it’s more performant to actually use index.
Also, tried to limit amount of re-layouts caused by simple text update. Added overflow: hidden where possible to at least simplify
Browser layout calculations. To fully get rid of it, I would need to change strategy to calculate each position manually, and it seems
like doing that seems like an overkill. However, if grid was intended to scale to thousands of items, different strategy would be used then
current one (virtualization, canvas, manual position calculation and transforms).
Alternative option could also be rendering size bars using canvas. But this seems like overkill for a problem, so didn’t pursue it.
If I had more time, I would play with idea of trying out different performance strategies based on rows count. There might a point
where some approach have advantage over another.
Another idea for later would be splitting up rendering steps into two. However, not sure how to do it
without affecting business requirements/sorting, etc. But it would help on low-end devices sometime struggling to handle
bigger updates redrawing effectively every row/cell.
Tech Stack
- NX – project monorepo setup bootstrapping
- Yarn – node modules package management
- React – Frontend rendering library
- Typescript – Building and strong typing
- Styled Components – CSS in JS
- Redux – State management
- Redux-Toolkit – Helper functionalities for State Management
- Redux-Toolkit Query – Redux ‘framework’ for defining standardized redux/store handling for data fetching and streaming.
- WebSocket – Data streaming
- Jest – Unit tests
- Cypress – E2E and Integration tests
Tested Devices
Moto G7 Play | Low End | 2019
Stable 60fps.
Pixel 2 | 2017
Stable 55-60fps.
Redmi Note 8 | 2019
Stable 60fps.
Samsung s8 | 2017
Stable 55-60fps.
Samsung s9 | 2018
Stable 60fps.
Dependency Graph
You can run below command to see dependency graph for the whole project
yarn nx dep-graph --watch=true
Then visit: http://127.0.0.1:4211
Available scripts commands
start
– Start development watch serverstart:production
– Start production server. Use to test production buildssanity
– Runs all validation checks (unit/e2e/builds/lint)build
– Build production version of default app which is currently trading-platform applint
– Run lint on all projects and librariestest
– Run all unit/jest tests from all projects and librariestest:coverage
– Build unit/jest coverage for all projects and librariese2e
– Build production version of default app and run e2e tests headless-ly for ite2e:watch
– Start development server with watch and run e2e in window modee2e:coverage
– Generate coverage for e2e tests on production version of default appcoverage
– Build and merge coverage for all projects and libraries. Currently, e2e is not merged due build setup differences between app and libs
You can also use all available nx commands for managing and working with monorepo workspace.