Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Lean	React	-
Patterns	for	High	Performance
Devon	Bernard
VP	of	Engineering	@	Enlitic
What	if	all	websites	had
airplane	mode
ReactJS
Why?
1. Increases	developer	productivity
• Separation	of	concerns	between	state	and	DOM
• More	modular	and	isolates	busine...
React	+	Redux	Lifecycle
Redux
Persisting	State
Why
• Smoother	UX
• Less	network	requests
How
• Redux	store	is	only	in-memory,	disappears	on	page	refresh...
Persisting	State	(2)
export const saveState = (state) => {
try {
const serializedState = JSON.stringify(state);
localStora...
Persisting	State	(3)
import { loadState, saveState } from './localStorage';
// import throttle from 'lodash/throttle';
con...
const libraries = [
{
id: 1,
city: 'New York'
},
{
id: 2,
city: 'San Francisco'
}
];
const libraries = [
{
id: 1, city: 'New York',
books: [
{
id: 1, title: 'Moby Dick',
author: 'Herman Melville'
}, {
id: 2,...
const libraries = [
{
id: 1, city: 'New York',
books: [
{
id: 1, title: 'Moby Dick',
author: {name: 'Herman Melville’, ......
const libraries = [
{
id:1, city: 'New York',
books: [1, 2]
}, {
id: 2, city: 'San Francisco',
books: [2,3]
}
];
const boo...
const libraries = {
1: { city: 'New York', books: [1,2] },
2: { city: 'San Francisco', books: [2,3] }
};
const books {
1: ...
Normalized	State
Why
• Faster	queries
• Less	data	redundancy
• More	flexible	entity	access
• Easier	to	test	&	debug
How
1....
Normalized	State	(2)
https://github.com/paularmstrong/normalizr
{
”id": "123",
"author": {
"id": "1",
”name": "Paul"
},
"t...
Pure	Reducers
Why
• Easier	to	debug
• Easier	to	test
How
• Cannot	depend	on	hidden	or	external	state
• Only	use	the	passed...
Redux	Dev	Tools
Redux	Dev	tools	(2)
Redux	Dev	tools	(3)
Components
Data	Heavy	Pages
Data	Heavy	Pages
2-4	seconds	later…
!
Data	Heavy	Pages
2-4	seconds	later…
!
Why?	Blockers.
class Dashboard extends Component {
render() {
const { stats } = this.props;
if (!stats) {
return null;
}
r...
Not	Only	Slow,	But	Dangerous	Too
X
Not	Only	Slow,	But	Dangerous	Too
Unblocking
class Dashboard extends Component {
render() {
const { stats } = this.props;
//if (!stats) {
// return null;
//...
…better
…better
…better
…better
X X
Component	Skeletons
Component	Skeletons	(2)
class Dashboard extends Component {
render() {
const { stats } = this.props;
return (
<div>
<StatW...
Component	Skeletons	(3)
class StatWidget extends Component {
render() {
const { stats:info } = this.props;
return (
<div>
...
…best
…best
…best
…best
Component	Lifecycle
shouldComponentUpdate?
Tracking	Repaints
Chrome	Render	Tools
Chrome	Render	Tools	(2)
Method	Binding
^^	Uncaught	TypeError:	Cannot	read	
property	'setColor'	of	null
class X extends Component {
setupDrawing() ...
Method	Binding	(2)
class X extends Component {
setupDrawing() {
this.setColor();
}
render() {
return (
<button
type="butto...
Method	Binding	(3)
class X extends Component {
setupDrawing = () => {
this.setColor();
}
render() {
return (
<button
type=...
Method	Binding	(4)
class X extends Component {
constructor(props) {
super(props);
this.setupDrawing = this.setupDrawing.bi...
Actions
ActionTypes
store.dispatch({
type: ”EDIT_TASK":,
payload: {}
});
actions.js
reducers.js
switch (action.type) {
case ”EDIT_...
ActionTypes (2)
const THUNK_TYPES = ['EDIT_TASK']; // network actions
const ACTION_TYPES = ['CLEAR_TASK']; // local action...
ActionTypes (3)
store.dispatch({
type: ActionTypes.tasks.edit,
payload: {}
});
actions.js
reducers.js
switch (action.type)...
0
1
2
3
start end
Thread
Time
Action	Chains	/	Promises
Action	A Action	B Action	C Action	D Action	E
Synchronous	Actions
0
1
2
3
start end
Thread
Time
Action	A
Action	B
Action	C
Fully	Asynchronous	Actions
Action	Chains	/	Promises	(2)
0
1
2
3
start end
Thread
Time
Partially	Asynchronous	Actions	(	.all(),	.any()	)
Action	Chains	/	Promises	(3)
Action	A
Acti...
store.getState()
What
Useful	for	exposing	store	state	to	any	
part	of	your	application
Why
We	want	to	keep	our	reducers	pu...
General
Environment	Files
REACT_APP_API_HOST='https://staging.myapp.com'
REACT_APP_API_HOST='http://localhost:5000'
.env
.env.loca...
Route	Wrappers
<Route path="/" component={Home} />
<Route path="/login" component={Login} />
<EnsureLoggedInContainer>
<Ro...
Route	Wrappers
class EnsureLoggedInContainer extends Component {
componentDidMount() {
if (!this.props.session.userID) {
c...
Offline	First	– Web	Workers
Javascript that	runs	in	the	background	that	can	be	used	for	hijacking	network	requests	and	cac...
Offline	First	– IndexedDB Promised
Like	localStorage,	but	able	to	store	larger	volumes	of	data	and	has	an	asynchronous	API...
Guess	that	utility!
• Get	your	entire	team	to	use	the	same	coding	standard
• Prevent	developers	from	shipping	un-optimized...
ESLint
$ npm run lint
/ProjectsX/client/src/js/reducers/index.js
125:9 error Identifier 'some_random_var' is not in camel ...
ESLint (2)
https://eslint.org/docs/rules/no-unused-vars
ESLint
https://github.com/airbnb/javascript
$ npm install --save-dev eslint eslint-config-airbnb eslint-plugin-jsx-a11y es...
Devon	Bernard
VP	of	Engineering	@	Enlitic
" devon@enlitic.com	
# @devonwbernard
Thank	you!
Any	questions?
Upcoming SlideShare
Loading in …5
×

2

Share

Download to read offline

Lean React - Patterns for High Performance [ploneconf2017]

Download to read offline

Modern web tools are enabling developers to build web apps in no-wifi and low-fi environments. This talk is about performance and how frontend engineers can create a better user experience while using less resources.

Talk presented on Oct 20, 2017 at PloneConf2017.

Topics:
Airplane mode for web apps:
Mobile and native apps get lots of praise for offline mode. But why aren’t we seeing more web-apps using this. Two reasons: persistent data and code storage. We can now provide offline apps with Web Workers and IndexedDB that also improve the smoothness and robustness of our web apps in high bandwidth environments.

Redux:
Persisting state, store middleware, store subscriptions, normalizing data, normalizr, pure reducers, redux dev tools

Components:
Data heavy pages, component blocks, component skeletons, component lifecycle, shouldComponentUpdate, virtual dom, chrome render tools, method binding

Actions:
ActionTypes, action chaining, promises, store.getState()

General:
env files, route wrappers, offline first, web workers, indexeddb, eslint, airbnb style guide



Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all

Lean React - Patterns for High Performance [ploneconf2017]

  1. 1. Lean React - Patterns for High Performance Devon Bernard VP of Engineering @ Enlitic
  2. 2. What if all websites had airplane mode
  3. 3. ReactJS
  4. 4. Why? 1. Increases developer productivity • Separation of concerns between state and DOM • More modular and isolates business logic by component (not by page) 2. Faster and smoother rendering of page elements
  5. 5. React + Redux Lifecycle
  6. 6. Redux
  7. 7. Persisting State Why • Smoother UX • Less network requests How • Redux store is only in-memory, disappears on page refresh/close • Use localStorage
  8. 8. Persisting State (2) export const saveState = (state) => { try { const serializedState = JSON.stringify(state); localStorage.setItem('state', serializedState); localStorage.setItem('updatedAt', (new Date).getTime()); } catch (err) { // Ignore write errors. } } export const loadState = () => { try { const serializedState = localStorage.getItem('state'); if (serializedState === null) { return undefined; } return JSON.parse(serializedState); } }; localStorage.js localStorage.js
  9. 9. Persisting State (3) import { loadState, saveState } from './localStorage'; // import throttle from 'lodash/throttle'; const middleware = applyMiddleware(promise(), thunk, loadState); const store = createStore(reducers, middleware); store.subscribe(() => { const currentTime = (new Date).getTime(); const updateTime = localStorage.getItem('updatedAt'); const { reducerA, reducerB } = store.getState(); if (currentTime - updateTime > 10000) { saveState({ reducerA, reducerB }); } }); store.js
  10. 10. const libraries = [ { id: 1, city: 'New York' }, { id: 2, city: 'San Francisco' } ];
  11. 11. const libraries = [ { id: 1, city: 'New York', books: [ { id: 1, title: 'Moby Dick', author: 'Herman Melville' }, { id: 2, title: 'The Odyssey', author: 'Homer' } ] }, { id: 2, city: 'San Francisco', books: [ { id: 2, title: 'The Odyssey', author: 'Homer' } ... ] } ...];
  12. 12. const libraries = [ { id: 1, city: 'New York', books: [ { id: 1, title: 'Moby Dick', author: {name: 'Herman Melville’, ...} }, { id: 2, title: 'The Odyssey', author: {name: 'Homer’, ...} } ] }, { id: 2, city: 'San Francisco', books: [ { id: 2, title: 'The Odyssey', author: {name: 'Homer’, ...} },... ] } ...];
  13. 13. const libraries = [ { id:1, city: 'New York', books: [1, 2] }, { id: 2, city: 'San Francisco', books: [2,3] } ]; const books = [ { id: 1, title: 'Moby Dick', author: 1 }, { id: 2, title: 'The Odyssey', author: 2} ]; const authors = [ { id: 1, name: 'Herman Melville', ...}, { id: 2, name: 'Homer', ...} ];
  14. 14. const libraries = { 1: { city: 'New York', books: [1,2] }, 2: { city: 'San Francisco', books: [2,3] } }; const books { 1: { title: 'Moby Dick', author: 1 }, 2: { title: 'The Odyssey', author: 2 } }; const authors = { 1: { name: 'Herman Melville', ...}, 2: { name: 'Homer', ...} };
  15. 15. Normalized State Why • Faster queries • Less data redundancy • More flexible entity access • Easier to test & debug How 1. Flatten reducers 2. Separate reducer entities (ideally to their own reducer) 3. Use objects with ids for keys instead of a list
  16. 16. Normalized State (2) https://github.com/paularmstrong/normalizr { ”id": "123", "author": { "id": "1", ”name": "Paul" }, "title": "My awesome blog post", "comments": [ { "id": "324", "commenter": { "id": "2", "name": "Nicole" } } ] } + import { normalize, schema } from 'normalizr'; const user = new schema.Entity( 'users'); const comment = new schema.Entity( 'comments', { commenter: user }); const article = new schema.Entity( 'articles', { author: user, comments: [ comment ] }); const normalizedData = normalize( originalData, article); { result: "123", entities: { "articles": { "123": { id: "123", author: "1", title: "My awesome blog post", comments: [ "324" ] } }, "users": { "1": { "id": "1", "name": "Paul" }, "2": { "id": "2", "name": "Nicole" } }, "comments": { "324": { id: "324", "commenter": "2" } }}} =>
  17. 17. Pure Reducers Why • Easier to debug • Easier to test How • Cannot depend on hidden or external state • Only use the passed parameters • Does NOT have side effects • E.g. overriding parameters
  18. 18. Redux Dev Tools
  19. 19. Redux Dev tools (2)
  20. 20. Redux Dev tools (3)
  21. 21. Components
  22. 22. Data Heavy Pages
  23. 23. Data Heavy Pages 2-4 seconds later… !
  24. 24. Data Heavy Pages 2-4 seconds later… !
  25. 25. Why? Blockers. class Dashboard extends Component { render() { const { stats } = this.props; if (!stats) { return null; } return ( <div> <StatWidget stats={stats.twitter} /> <StatWidget stats={stats.facebook} /> <StatWidget stats={stats.google} /> <StatWidget stats={stats.linkedin} /> <StatWidget stats={stats.email} /> </div> ) } }
  26. 26. Not Only Slow, But Dangerous Too X
  27. 27. Not Only Slow, But Dangerous Too
  28. 28. Unblocking class Dashboard extends Component { render() { const { stats } = this.props; //if (!stats) { // return null; //} return ( <div> { stats.twitter && <StatWidget stats={stats.twitter} /> } { stats.facebook && <StatWidget stats={stats.facebook} /> } { stats.google && <StatWidget stats={stats.google} /> } { stats.linkedin && <StatWidget stats={stats.linkedin} /> } { stats.email && <StatWidget stats={stats.email} /> } </div> ) } }
  29. 29. …better
  30. 30. …better
  31. 31. …better
  32. 32. …better X X
  33. 33. Component Skeletons
  34. 34. Component Skeletons (2) class Dashboard extends Component { render() { const { stats } = this.props; return ( <div> <StatWidget stats={stats.twitter || undefined} /> <StatWidget stats={stats.facebook || undefined} /> <StatWidget stats={stats.google || undefined} /> <StatWidget stats={stats.linkedin || undefined} /> <StatWidget stats={stats.email || undefined} /> </div> ) } } dashboard.js
  35. 35. Component Skeletons (3) class StatWidget extends Component { render() { const { stats:info } = this.props; return ( <div> <div className="widget-header"> {info.title} </div> <div className="widget-body"> { info.icon && <img src={info.icon} /> } <div className="widget-text"> { info.followers } </div> </div> </div> ) } } statWidget.js StatWidget.propTypes = { stats: PropTypes.object } StatWidget.defaultProps = { stats: { title: 'Loading widget', followers: ’’, icon: 'img/spinner.gif' } } statWidget.js
  36. 36. …best
  37. 37. …best
  38. 38. …best
  39. 39. …best
  40. 40. Component Lifecycle
  41. 41. shouldComponentUpdate?
  42. 42. Tracking Repaints
  43. 43. Chrome Render Tools
  44. 44. Chrome Render Tools (2)
  45. 45. Method Binding ^^ Uncaught TypeError: Cannot read property 'setColor' of null class X extends Component { setupDrawing() { // Some logic this.setColor(); // More logic } setColor() { ... } render() { return ( <button type="button" onClick={this.setupDrawing} /> ) } } Issue Callbacks and event handlers are passing a different scope and ‘this’ to your component methods
  46. 46. Method Binding (2) class X extends Component { setupDrawing() { this.setColor(); } render() { return ( <button type="button" onClick={this.setupDrawing.bind(this)} /> ) } } Method 1 (Inline Binding)
  47. 47. Method Binding (3) class X extends Component { setupDrawing = () => { this.setColor(); } render() { return ( <button type="button" onClick={this.setupDrawing} /> ) } } Method 2 (Fat Arrow Syntax)
  48. 48. Method Binding (4) class X extends Component { constructor(props) { super(props); this.setupDrawing = this.setupDrawing.bind(this); } setupDrawing() { this.setColor(); } render() { return ( <button type="button" onClick={this.setupDrawing} /> ) } } Method 3 (Constructor)
  49. 49. Actions
  50. 50. ActionTypes store.dispatch({ type: ”EDIT_TASK":, payload: {} }); actions.js reducers.js switch (action.type) { case ”EDIT_TASK_PENDING": case "EDIT_TASK_FULFILLED": case ”EDIT_TASK_REJECTED": }
  51. 51. ActionTypes (2) const THUNK_TYPES = ['EDIT_TASK']; // network actions const ACTION_TYPES = ['CLEAR_TASK']; // local actions // Combine all local and network actions class ActionTypes { constructor() { THUNK_TYPES.forEach((action) => { this[action] = action; this[`${action}_PENDING`] = `${action}_PENDING`; this[`${action}_REJECTED`] = `${action}_REJECTED`; this[`${action}_FULFILLED`] = `${action}_FULFILLED`; }); ACTION_TYPES.forEach((action) => { this[action] = action; }); } }; const actionTypes = new ActionTypes(); export default actionTypes; store.dispatch({ type: ActionTypes.EDIT_TASK, payload: {} }); actions.js reducers.js switch (action.type) { case ActionTypes.EDIT_TASK_PENDING: case ActionTypes.EDIT_TASK_FULFILLED: case ActionTypes.EDIT_TASK_REJECTED: }
  52. 52. ActionTypes (3) store.dispatch({ type: ActionTypes.tasks.edit, payload: {} }); actions.js reducers.js switch (action.type) { case ActionTypes.tasks.edit.pending: case ActionTypes.tasks.edit.fulfilled: case ActionTypes.tasks.edit.rejected: }
  53. 53. 0 1 2 3 start end Thread Time Action Chains / Promises Action A Action B Action C Action D Action E Synchronous Actions
  54. 54. 0 1 2 3 start end Thread Time Action A Action B Action C Fully Asynchronous Actions Action Chains / Promises (2)
  55. 55. 0 1 2 3 start end Thread Time Partially Asynchronous Actions ( .all(), .any() ) Action Chains / Promises (3) Action A Action B Action C
  56. 56. store.getState() What Useful for exposing store state to any part of your application Why We want to keep our reducers pure, so we enable action creators to pass all required parameters export function loadTaskQuestions(taskID) { store.dispatch({ type: ActionTypes.LOAD_TASK_QUESTIONS, payload: axiosInstance.get(`/task_questions/${taskID}`) }).then(() => { const currentTask = store.getState().currentTask; if (currentTask.status === 'done') { loadTaskAnswers(currentTask.id); } else { populateTaskAnswers(); } }); } actions.js
  57. 57. General
  58. 58. Environment Files REACT_APP_API_HOST='https://staging.myapp.com' REACT_APP_API_HOST='http://localhost:5000' .env .env.local const axiosInstance = axios.create({ baseURL: process.env.REACT_APP_API_HOST, withCredentials: true }); actions.js
  59. 59. Route Wrappers <Route path="/" component={Home} /> <Route path="/login" component={Login} /> <EnsureLoggedInContainer> <Route path="/profile" component={Profile} /> <Route path="/settings" component={Settings} /> </EnsureLoggedInContainer> router.js
  60. 60. Route Wrappers class EnsureLoggedInContainer extends Component { componentDidMount() { if (!this.props.session.userID) { const userID = localStorage.getItem('user-id'); if (userID) { Actions.setUserID(parseInt(userID, 10)); } else { this.props.history.push('/login'); } } } render() { if (this.props.session.userID) { return this.props.children; } return null; } } ensureLoggedInContainer.js
  61. 61. Offline First – Web Workers Javascript that runs in the background that can be used for hijacking network requests and caching source code var staticCacheName = ’my-app-v1'; self.addEventListener('install', (event) => { self.skipWaiting(); event.waitUntil( caches.open(staticCacheName) .then((cache) => { return cache.addAll([ './', 'js/bundle.min.js', 'css/main.css', 'imgs/CaltrainMap.png' ]); }) ); }); webworker.js if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.min.js') } index.js
  62. 62. Offline First – IndexedDB Promised Like localStorage, but able to store larger volumes of data and has an asynchronous API. let dbPromise = idb.open(’app-db', 1, function(upgradeDb) { switch(upgradeDb.oldVersion) { case 0: let lineStore = upgradeDb.createObjectStore('lines'); } }); class DB { constructor() {} storeLine(line) { dbPromise.then((db) => { let tx = db.transaction('lines', 'readwrite'); let lineStore = tx.objectStore('lines'); lineStore.put(line, line.Id); return tx.complete; }); } } idbController.js
  63. 63. Guess that utility! • Get your entire team to use the same coding standard • Prevent developers from shipping un-optimized code • Educate your team about best practices and how to improve their code • 100% free
  64. 64. ESLint $ npm run lint /ProjectsX/client/src/js/reducers/index.js 125:9 error Identifier 'some_random_var' is not in camel case camelcase 125:9 warning 'some_random_var' is assigned a value but never used no-unused-vars 134:7 error for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array no-restricted-syntax ✖ 3 problems (2 errors, 1 warning)
  65. 65. ESLint (2) https://eslint.org/docs/rules/no-unused-vars
  66. 66. ESLint https://github.com/airbnb/javascript $ npm install --save-dev eslint eslint-config-airbnb eslint-plugin-jsx-a11y eslint-plugin-react module.exports = { "parser": "babel-eslint", "extends": "airbnb", "plugins": [ "react", "jsx-a11y” ], "rules": {} }; .eslintrc
  67. 67. Devon Bernard VP of Engineering @ Enlitic " devon@enlitic.com # @devonwbernard Thank you! Any questions?
  • NngThn1

    Apr. 7, 2020
  • MaborakTechnologies

    Nov. 8, 2017

Modern web tools are enabling developers to build web apps in no-wifi and low-fi environments. This talk is about performance and how frontend engineers can create a better user experience while using less resources. Talk presented on Oct 20, 2017 at PloneConf2017. Topics: Airplane mode for web apps: Mobile and native apps get lots of praise for offline mode. But why aren’t we seeing more web-apps using this. Two reasons: persistent data and code storage. We can now provide offline apps with Web Workers and IndexedDB that also improve the smoothness and robustness of our web apps in high bandwidth environments. Redux: Persisting state, store middleware, store subscriptions, normalizing data, normalizr, pure reducers, redux dev tools Components: Data heavy pages, component blocks, component skeletons, component lifecycle, shouldComponentUpdate, virtual dom, chrome render tools, method binding Actions: ActionTypes, action chaining, promises, store.getState() General: env files, route wrappers, offline first, web workers, indexeddb, eslint, airbnb style guide

Views

Total views

863

On Slideshare

0

From embeds

0

Number of embeds

118

Actions

Downloads

8

Shares

0

Comments

0

Likes

2

×