Rendering Large Datasets With Angular 2 Beta 3 And ReactJS 0.14.7

Last year, when I was digging into ReactJS, I set up a little demo that compared the loading and processing of large datasets in both ReactJS 0.13 and AngularJS 1.4.2. Now that Angular 2 is in Beta 3, and the Angular team is reporting a 3-10x performance improvement, I wanted to revisit this exploration to see what that means for the AngularJS side of this experiment.

Run this demo in my JavaScript Demos project on GitHub.

WARNING: Both ReactJS and AngularJS have changed in the time since I last looked at this demo. I am not ReactJS developer; but, I have done my best to update the ReactJS portion to use the latest ReactJS 0.14.7 and vanilla JavaScript classes instead of the former React.createClass() approach. That said, I am still using the 0.13 JSXTransformer so that I can keep the entire demo on the same page. My assumption is such that using the JSX transformer will only affect the initial page load but should become irrelevant after that. Of course, I am not entirely sure that this is true.

I have also put the Angular 2 library into "production mode" in order to disable the various assertions that it makes during the application life-cycle. I am doing my best to create an even playing field for the two portions of the demo.

The demo, itself, has not changed. I am presenting the user with a filter form and a grid of 10,000 items (1,000 rows with 10 columns). As the user changes the filter query, matching cells within the grid are highlighted. There is also a link to unmount and remount the grid in an effort to separate update time from DOM (Document Object Model) creation time.

When we last explored this comparison, ReactJS was the clear winner in all categories of performance, though I always found the AngularJS code easier to reason about. With Angular 2 Beta 3, the performance is split. ReactJS is still clearly faster when it comes to the initial rendering and the re-mounting of the grid. But, I am pleased to say that Angular 2 Beta 3 is clearly faster when it comes to applying the dynamic filter to the existing grid.

Here is the Angular 2 Beta 3 code:

<!doctype html> <html> <head> <meta charset="utf-8" /> <title> Rendering Large Datasets With Angular 2 Beta 3 </title> <link rel="stylesheet" type="text/css" href="./demo.css"></link> </head> <body> <h1> Rendering Large Datasets With Angular 2 Beta 3 </h1> <my-app> Loading... </my-app> <!-- Load demo scripts. --> <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/es6-shim.min.js"></script> <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/Rx.umd.min.js"></script> <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/angular2-polyfills.min.js"></script> <!-- CAUTION: This demo does not work with the minified UMD code. --> <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/angular2-all.umd.js"></script> <!-- AlmondJS - minimal implementation of RequireJS. --> <script type="text/javascript" src="../../vendor/angularjs-2-beta/3/almond.js"></script> <script type="text/javascript"> // Enabling production mode to make sure there's no performance degradation // due to assertions and other checks the framework might be doing during the // execution of the application. ng.core.enableProdMode(); // Defer bootstrapping until all of the components have been declared. // -- // NOTE: Not all components have to be required here since they will be // implicitly required by other components. requirejs( [ "AppComponent" ], function run( AppComponent ) { ng.platform.browser.bootstrap( AppComponent ); } ); // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // I provide the root App component. define( "AppComponent", function registerAppComponent() { // Configure the app component definition. ng.core .Component({ selector: "my-app", template: ` <form> <strong>Filter Data</strong>: <input type="text" [(ngModel)]="form.filter" (input)="handleFilterChange( $event.target.value )" /> <span *ngIf="form.filter"> — Filtering <strong>{{ form.filter }}</strong> over {{ dataPoints }} data points, {{ visibleCount }} found. </span> <a *ngIf="grid.length" (click)="unmountGrid()">Unmount Grid</a> <a *ngIf="! grid.length" (click)="remountGrid()">Remount Grid</a> </form> <table width="100%" cellspacing="2" [class.filtered]="form.filter"> <tr *ngFor="#row of grid"> <td> {{ row.id }} </td> <td *ngFor="#item of row.items" class="item" [class.hidden]="item.isHiddenByFilter"> {{ item.value }} </td> </tr> </table> ` }) .Class({ constructor: AppController }) ; return( AppController ); // I control the App component. function AppController() { var vm = this; // We'll start out with a grid with 10,000 items. vm.grid = generateGrid( 1000, 10 ); // Calculate the number of data-points that may have filtering. vm.dataPoints = ( vm.grid.length * vm.grid[ 0 ].items.length ); // I hold the number of items that are visible based on filtering. vm.visibleCount = 0; // I hold the form data for use with ngModel. vm.form = { filter: "" }; // Expose the public API. vm.handleFilterChange = handleFilterChange; vm.remountGrid = remountGrid; vm.unmountGrid = unmountGrid; // --- // PUBLIC METHODS. // --- // I update the visibility of the items when the filter is updated. // -- // CAUTION: The actual value of input is being maintained via ngModel. // We're just responding to the change event. function handleFilterChange( newValue ) { // Reset the visible count. As we iterate of the items checking // for visibility, we can increment this count as necessary. vm.visibleCount = 0; // We are pre-calculating the column count here because we are // assuming a uniform column distribution in the grid. var rowCount = vm.grid.length; var columnCount = ( vm.grid.length && vm.grid[ 0 ].items.length ); for ( var r = 0 ; r < rowCount ; r++ ) { var row = vm.grid[ r ]; for ( var c = 0 ; c < columnCount ; c++ ) { var item = row.items[ c ]; // The item is hidden if the given filter text cannot be // found in the value of the item. item.isHiddenByFilter = ( newValue && ( item.value.indexOf( newValue ) === -1 ) ); // If the item isn't hidden, track it as part of the visible // set of data. if ( ! item.isHiddenByFilter ) { vm.visibleCount++; } } } } // I repopulate the grid with data. This will help separate processing // performance characteristics from page-load processing. function remountGrid() { vm.grid = generateGrid( 1000, 10 ); vm.dataPoints = ( vm.grid.length * vm.grid[ 0 ].items.length ); vm.visibleCount = 0; vm.form.filter = ""; } // I clear the grid of data. This will help separate processing // performance characteristics from page-load processing. function unmountGrid() { vm.grid = []; vm.dataPoints = 0; vm.visibleCount = 0; vm.form.filter = ""; } // --- // PRIVATE METHODS. // --- // I generate a grid of items with the given dimensions. The grid is // represented as a two dimensional grid, of sorts. Each row has an // object that has an items collection. function generateGrid( rowCount, columnCount ) { var valuePoints = [ "Daenerys", "Jon", "Sansa", "Arya", "Stannis", "Gregor", "Tyrion", "Theon", "Joffrey", "Ramsay", "Cersei", "Bran", "Margaery", "Melisandre", "Daario", "Jamie", "Eddard", "Myrcella", "Robb", "Jorah", "Petyr", "Tommen", "Sandor", "Oberyn", "Drogo", "Ygritte" ]; var valueIndex = 0; var grid = []; for ( var r = 0 ; r < rowCount ; r++ ) { var row = { id: r, items: [] }; for ( var c = 0 ; c < columnCount ; c++ ) { row.items.push({ id: ( r + "-" + c ), value: valuePoints[ valueIndex ], isHiddenByFilter: false }); if ( ++valueIndex >= valuePoints.length ) { valueIndex = 0; } } grid.push( row ); } return( grid ); } } } ); </script> </body> </html>

While I am new to Angular 2, I am also quite the noob when it comes to ReactJS. And, this is the first time that I have ever even used ReactJS 0.14.x. So, hopefully I didn't screw this up too much or include code that puts ReactJS at an unfair advantage.

Here is the ReactJS 0.14.7 code:

<!doctype html> <html> <head> <meta charset="utf-8" /> <title> Rendering Large Datasets With ReactJS 0.14.7 </title> <link rel="stylesheet" type="text/css" href="./demo.css"></link> </head> <body> <h1> Rendering Large Datasets With ReactJS 0.14.7 </h1> <div id="content"> <!-- This content will be replaced with the React rendering. --> </div> <!-- Load scripts. --> <script src="../../vendor/reactjs/0.14.7/react.min.js"></script> <!-- CAUTION: Using the JSXTransformer for in-browser transpiling - this is deprecated and not recommended with 0.14.7. But, this should only affect the initial page load, not the post-rendering performance (my assumption). --> <script src="../../vendor/reactjs/JSXTransformer-0.13.3.js"></script> <script type="text/jsx"> // Demo has to extend Component since its a stateful component. Demo.prototype = Object.create( React.Component.prototype ); // I manage the demo. function Demo( initialProps ) { // Call the super constructor. React.Component.call( this, initialProps ); var vm = this; // I provide the initial view-model, before the component is mounted. vm.state = { grid: generateGrid( 1000, 10 ), form: { filter: "" } }; // Expose the public methods. vm.render = render; vm.remountGrid = remountGrid; vm.setFilter = setFilter; vm.unmountGrid = unmountGrid; // --- // PUBLIC METHODS. // --- // I render the view using the current state and properties collections. function render() { // Calculate how many data-points are being rendered (ie, how many tables // cells we'll be filtering during the demo). if ( vm.state.grid.length ) { var dataPoints = ( vm.state.grid.length * vm.state.grid[ 0 ].items.length ); } else { var dataPoints = 0; } // Determine how many items are being highlighted by the search. var visibleCount = getVisibleCount(); return( <div> <DemoForm dataPoints={ dataPoints } visibleCount={ visibleCount } filter={ vm.state.form.filter } onFilterChange={ setFilter } isMounted={ !! vm.state.grid.length } onUnmount={ unmountGrid } onRemount={ remountGrid }> </DemoForm> <DemoTable grid={ vm.state.grid } filter={ vm.state.form.filter }> </DemoTable> </div> ); } // I repopulate the grid with data. This will help separate processing // performance characteristics from page-load processing. function remountGrid() { vm.setState({ grid: generateGrid( 1000, 10 ), form: { filter: "" } }); } // I update the state for filtering. function setFilter( newFilter ) { // When we update the filter, we don't have to mutate any other state // since the filtering is actually applied in the render() methods. vm.setState({ form: { filter: newFilter } }); } // I clear the grid of data. This will help separate processing performance // characteristics from page-load processing. function unmountGrid() { vm.setState({ grid: [], form: { filter: "" } }); } // --- // PRIVATE METHODS. // --- // I generate a grid of items with the given dimensions. The grid is // represented as a two dimensional grid, of sorts. Each row has an object // that has an items collection. function generateGrid( rowCount, columnCount ) { var valuePoints = [ "Daenerys", "Jon", "Sansa", "Arya", "Stannis", "Gregor", "Tyrion", "Theon", "Joffrey", "Ramsay", "Cersei", "Bran", "Margaery", "Melisandre", "Daario", "Jamie", "Eddard", "Myrcella", "Robb", "Jorah", "Petyr", "Tommen", "Sandor", "Oberyn", "Drogo", "Ygritte" ]; var valueIndex = 0; var grid = []; for ( var r = 0 ; r < rowCount ; r++ ) { var row = { id: r, items: [] }; for ( var c = 0 ; c < columnCount ; c++ ) { row.items.push({ id: ( r + "-" + c ), value: valuePoints[ valueIndex ], isHiddenByFilter: false }); if ( ++valueIndex >= valuePoints.length ) { valueIndex = 0; } } grid.push( row ); } return( grid ); } // I calculate and return the visible count of items based on the current // state of the filtering. function getVisibleCount() { var count = 0; // We are pre-calculating the column count here because we are assuming // a uniform column distribution in the grid. var rowCount = vm.state.grid.length; var columnCount = ( vm.state.grid.length && vm.state.grid[ 0 ].items.length ); for ( var r = 0 ; r < rowCount ; r++ ) { var row = vm.state.grid[ r ]; for ( var c = 0 ; c < columnCount ; c++ ) { var item = row.items[ c ]; var isHidden = ( vm.state.form.filter && ( item.value.indexOf( vm.state.form.filter ) === -1 ) ); if ( ! isHidden ) { count++; } } } return( count ); } } // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // I manage the Form widget. // -- // NOTE: We don't need to extend the Render.Component class since this is not // a stateful component - it's rendering is performed exclusively using the // passed-in props. function DemoForm( initialProps ) { var vm = this; // Expose the public methods. vm.handleFilterChange = handleFilterChange; vm.handleRemount = handleRemount; vm.handleUnmount = handleUnmount; vm.render = render; // --- // PUBLIC METHODS. // --- // I handle user-based changes on the input form. When the user updates the // filtering, we need to let the calling context know about it. function handleFilterChange( event ) { vm.props.onFilterChange( vm.refs.filter.value ); } // I handle the user's desire to remount the data. function handleRemount( event ) { vm.props.onRemount(); } // I handle the user's desire to unmount the data. function handleUnmount( event ) { vm.props.onUnmount(); } // I render the view using the current state and properties collections. function render() { var fitlerInsight = null; // If the user has entered filter text, we want to show some insight into // the breadth of the filtering. // -- // CAUTION: We have to have these awkward and explicit spaces { " " } // because the JSX strips out certain pieces of whitespace, leaving // the input butted-up against the label. if ( vm.props.filter ) { fitlerInsight = ( <span> — Filtering <strong>{ vm.props.filter }</strong> { " " } over { vm.props.dataPoints } data points, { " " } { vm.props.visibleCount } found. </span> ); } // Provide some tooling to unmount and remount the data. if ( vm.props.isMounted ) { var mountAction = <a onClick={ handleUnmount }>Unmount Grid</a>; } else { var mountAction = <a onClick={ handleRemount }>Remount Grid</a>; } // CAUTION: We have to have these awkward and explicit spaces { " " } // because the JSX strips out certain pieces of whitespace, leaving // the input butted-up against the label. return( <form> <strong>Filter Data</strong>: { " " } <input type="text" ref="filter" value={ vm.props.filter } onChange={ handleFilterChange } /> { " " } { fitlerInsight } { " " } { mountAction } </form> ); } } // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // I manage the Table widget. // -- // NOTE: This component is just a render function. This uses the new stateless // component syntax introduce in 0.14. This syntax does not provide an actual // component instance behind the scenes. var DemoTable = function render( props ) { // If the table is being filtered, we want to add a class to the table to // set a default style for all the non-hidden elements. var tableClasses = props.filter ? "filtered" : null ; // Creating a local reference so we don't have to .bind() the iterator. var filter = props.filter; // Translate the grid into a collection of rows. var rows = props.grid.map( function transformRow( row ) { return( <DemoTableRow key={ row.id } row={ row } filter={ filter }> </DemoTableRow> ); } ); return( <table width="100%" cellSpacing="2" className={ tableClasses }> <tbody> { rows } </tbody> </table> ); }; // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // I manage the Table rows. // -- // NOTE: This component is just a render function. This uses the new stateless // component syntax introduce in 0.14. This syntax does not provide an actual // component instance behind the scenes. var DemoTableRow = function render( props ) { var columns = [ <td> { props.row.id } </td> ]; // Creating a local reference so we don't have to .bind() the iterator. var filter = props.filter; // Translate each item into a TD element. If there is filtering being // applied, some of the TD elements will have the "hidden" class. props.row.items.forEach( function transformItem( item ) { var classes = "item"; if ( filter && ( item.value.indexOf( filter ) === -1 ) ) { classes += " hidden"; } columns.push( <td key={ item.id } className={ classes }> { item.value } </td> ); } ); return( <tr> { columns } </tr> ); }; // --------------------------------------------------------------------------- // // --------------------------------------------------------------------------- // // Render the root Demo and mount it inside the given element. React.render( <Demo />, document.getElementById( "content" ) ); </script> </body> </html>

As someone who is infinitely more familiar with AngularJS, I still find the Angular code both easier to read and to reason about (when compared to ReactJS). I still think that React's lack of constructs for things like ngFor / ng-repeat require the developer to create an unnecessarily large number of tiny components. Or, to use .bind() during iteration which is a "code smell" to be avoided.

That said, I am definitely excited that AngularJS is clearly making improvements in performance. And, from what I've heard in various podcasts, the Angular team has more performance tweaking ahead of it in the roadmap. As far as I'm concerned, the future of Angular 2 is bright!

Tweet This Groovy post by @BenNadel - Rendering Large Datasets With Angular 2 Beta 3 And ReactJS 0.14.7 Woot woot — you rock the party that rocks the body!







