building applications€¦ · building applications for the next billion users. siddharth...
TRANSCRIPT
BUILDING APPLICATIONSFOR THE NEXT BILLION USERS
Siddharth Kshetrapal
@siddharthkp
@siddharthkp
javascript architect
@siddharthkp
past: @practo
bit.ly/siddharthkpshow
co-organise ReactBangalore
i teach react
bit.ly/react-training
340 millioninternet users in India
source
340 millioninternet users in India
entire population of USA = 320 million
source
340 millioninternet users in India
25% of the population
source
340 millioninternet users in India
+20% every quarter
source: akamai
state of mobile apps
source: akamai
within 30 days,the average app loses 85% of it’s DAUs
source: quettra
source: quettra
storage
8 - 16 GBaverage storage
from a sample size of 25L devices
source: helpchat/yourstory
source: helpchat/yourstory
typical storage split of a 8GB device
30%of helpchat’s uninstalls aredirectly related to storage
source: helpchat/yourstory
web is the answer
100%devices ship with an internet browser
web sites let you reach users, apps let you deeply engage users
state of mobile webin 2017
1. capabilities
fast/smooth
works offline
lives on your phone
native functionalities
fast/smooth
works offline
lives on your phone
native functionalities
fast/smooth
works offline
lives on your phone
native functionalities
Add to homescreen + app drawer
standalone window
fast/smooth
works offline
lives on your phone
native functionalities
notifications
geolocation
camera
background sync
speech recognition
upload files
bluetooth
nfc
record audio
vibration
battery state
offline storage
orientation
clipboard
notifications
geolocation
camera
background sync
speech recognition
sms
contacts
geofencing
proximity sensor
fast/smooth
works offline
lives on your phone
native functionalities
fast/smooth
works offline
lives on your phone
native functionalities
Progressive web apps
fast/smooth
works offline
lives on your phone
native functionalities
2. user metrics
web sites let you reach users, apps let you deeply engage users
3xtime spent on app vs website for Pinterest
source: venturebeat
27%conversion from app store
source: splitmetrics
selection bias
selection bias
1. the users are high intent users
selection bias
1. the users are high intent users
2. 73% of potential users spentexactly zero time in the app
270% more users
top 1000 websites vs top 1000 native apps
source: splitmetrics
promise of the mobile webin 2017
promise of the mobile web
capabilities of native+
reach of the web
problems with the web
340 millioninternet users in India
source
60%have 3G and slower networks
source: akamai
40%of practo’s mobile users
are on 3G and slower
63%of flipkart mobile users are on 2G
source : developers.google.com
source: fastcompany
fast/smooth
works offline
lives on your phone
native functionalities
“If you can’t measure it,you can’t improve it”
Measure on slow networks,that’s where your users are
Faston 3G (1.5Kb/s)
First meaningful paint 1.6s
Faston 3G (1.5Kb/s)
First meaningful paint 1.6s
Time to interactive 2.5s
Faston 3G (1.5Kb/s)
First meaningful paint 1.6s
Time to interactive 2.5s
Page ready 3.6s
HTML
I I I I I I I I I I I I I I I I I
JS download
HTML
CSS
I I I I I I I I I I I I I I I I I
JS download
HTML
CSS
parse
I I I I I I I I I I I I I I I I I
JS download
HTML
CSS
DATA
parse
I I I I I I I I I I I I I I I I I
JS download
HTML
CSS
DATA
parse
I I I I I I I I I I I I I I I I I
#1 render on the server
#1 render on the server
fetch data on low latency
render with high processing power
/* server.js */
import express from 'express'
import { renderToString } from 'react-dom/server'
import api from './api-service'
const server = express()
server.get('/medicine-info', (req, res) => {
api.get(req.path).then(data => {
res.end(renderToString(<App {...data} />))
})
}
/* server.js */
import express from 'express'
import { renderToString } from 'react-dom/server'
import api from './api-service'
const server = express()
server.get('/medicine-info', (req, res) => {
api.get(req.path).then(data => {
res.end(renderToString(<App {...data} />))
})
}
/* server.js */
import express from 'express'
import { renderToString } from 'react-dom/server'
import api from './api-service'
const server = express()
server.get('/medicine-info', (req, res) => {
api.get(req.path).then(data => {
res.end(renderToString(<App {...data} />))
})
}
/* server.js */
import express from 'express'
import { renderToString } from 'react-dom/server'
import api from './api-service'
const server = express()
server.get('/medicine-info', (req, res) => {
api.get(req.path).then(data => {
res.end(renderToString(<App {...data} />))
})
}
JS download
HTML
CSS
DATA
parse
I I I I I I I I I I I I I I I I I
JS download
CSS
DATA
parse
I I I I I I I I I I I I I I I I I
HTML
JS download
CSS
DATA
parse
I I I I I I I I I I I I I I I I I
HTML
JS download
CSS
DATA
parse
I I I I I I I I I I I I I I I I I
HTML
seo?
time-to-first-byte is delayed
source : viget
/* server.js */
import express from 'express'
import { renderToString } from 'react-dom/server'
import api from './api-service'
const server = express()
server.get('/medicine-info', (req, res) => {
res.write(staticHTML)
res.flush()
api.get(req.path).then(data => {
res.write(renderToString(<App {...data} />))
res.end()
})
}
/* server.js */
import express from 'express'
import { renderToString } from 'react-dom/server'
import api from './api-service'
const server = express()
server.get('/medicine-info', (req, res) => {
res.write(staticHTML)
res.flush()
api.get(req.path).then(data => {
res.write(renderToString(<App {...data} />))
res.end()
})
}
/* server.js */
import express from 'express'
import { renderToString } from 'react-dom/server'
import api from './api-service'
const server = express()
server.get('/medicine-info', (req, res) => {
res.write(staticHTML)
res.flush()
api.get(req.path).then(data => {
res.write(renderToString(<App {...data} />))
res.end()
})
}
api is slow?
source : viget
skeleton screens
JS download
CSS
DATA
parse
I I I I I I I I I I I I I I I I I
HTML
1.5xusers will wait longer on a custom loader
source : viget
First meaningful paint 1.6s
Time to interactive 2.5s
Page ready 3.6s
performance.mark('Page ready')
class Page extends React.Component {
constructor(props) { ... }
componentDidMount() {
performance.mark('Page ready')
}
render() { ... }
}
Load the minimum amount of javascript required for a page
split on page boundaries
#2
/* webpack.config.js */
export default {
/* Use pages as entry points */
entry: {
home: './container/home.js',
brand: './container/brand.js',
search: './container/search.js'
},
output: {
filename: '[name]-[chunkhash].js',
path: resolve('./dist')
}
}
/* webpack.config.js */
export default {
/* Use pages as entry points */
entry: {
home: './container/home.js',
brand: './container/brand.js',
search: './container/search.js'
},
output: {
filename: '[name]-[chunkhash].js',
path: resolve('./dist')
}
}
/* webpack.config.js */
export default {
/* Use pages as entry points */
entry: {
home: './container/home.js',
brand: './container/brand.js',
search: './container/search.js'
},
output: {
filename: '[name]-[chunkhash].js',
path: resolve('./dist')
}
}
Load the minimum amount of javascript required for a page
use smaller dependencies
#2
react + react-dom 34.9kB
preact 3.5kB
react + react-dom 34.9kB
preact + preact-compat 8.3kB
/* webpack.config.js */
export default {
resolve: {
alias: {
'react' : 'preact-compat',
'react-dom' : 'preact-compat'
}
}
}
#3 Lazy load good to have features
marketing banners, analytics, etc.
class Page extends React.Component {
constructor(props) { ... }
componentDidMount() {
const tag = document.createElement('script')
tag.src = 'https://analytics.com/script.js'
document.head.appendChild(tag)
}
render() { ... }
}
class Page extends React.Component {
constructor(props) { ... }
componentDidMount() {
const tag = document.createElement('script')
tag.src = 'https://analytics.com/script.js'
document.head.appendChild(tag)
}
render() { ... }
}
anticipation is the key to performance
#4 Prefetch + cache the next page
class Page extends React.Component {
constructor(props) { ... }
componentDidMount() {
navigator.serviceWorker.controller.postMessage({
method: 'cache',
urls: ['products', '/cart', 'checkout']
})
}
render() { ... }
}
class Page extends React.Component {
constructor(props) { ... }
componentDidMount() {
navigator.serviceWorker.controller.postMessage({
method: 'cache',
urls: ['products', '/cart', 'checkout']
})
}
render() { ... }
}
class Page extends React.Component {
constructor(props) { ... }
componentDidMount() {
navigator.serviceWorker.controller.postMessage({
method: 'cache',
urls: ['products', '/cart', 'checkout']
})
}
render() { ... }
}
/* sw.js */
self.addEventListener('message', event => {
if (event.data.method === 'cache') {
caches.open('website-cache').then(cache => {
return cache.addAll(event.data.urls)
})
}
})
/* sw.js */
self.addEventListener('message', event => {
if (event.data.method === 'cache') {
caches.open('website-cache').then(cache => {
return cache.addAll(event.data.urls)
})
}
})
/* sw.js */
self.addEventListener('message', event => {
if (event.data.method === 'cache') {
caches.open('website-cache').then(cache => {
return cache.addAll(event.data.urls)
})
}
})
#1 Render meaningful content on the server
#2 Load the min js required for a page/route
#3 Lazy load good-to-have features
#4 Prefetch + cache the next page
Image credit: Pawel Granatowski
how do you stay fast?
bundlesize
bundlesize
used by
preact lighthouse styled-components redux-saga react-apollo
/* package.json */
{
"name": "your-awesome-app",
"scripts": {
"test": "jest && bundlesize"
},
"bundlesize": [
{"path": "./dist/common-*.js", "maxSize": "25Kb"},
{"path": "./dist/feature-*.js","maxSize": "30Kb"}
]
}
/* package.json */
{
"name": "your-awesome-app",
"scripts": {
"test": "jest && bundlesize"
},
"bundlesize": [
{"path": "./dist/common-*.js", "maxSize": "25Kb"},
{"path": "./dist/feature-*.js","maxSize": "30Kb"}
]
}
fast/smooth
works offline
lives on your phone
native functionalities
navigator.serviceWorker.register('/sw.js')
install
activate
install
activate
fetch
message
sync
push
first run
second run
i. cache first
ii. network first
iii. prefetch
fast/smooth
works offline
lives on your phone
native functionalities
/* index.html */
<head>
<link rel="manifest" href="/manifest.json">
</head>
/* manifest.json */
{
"name": "your-awesome-app",
"short_name": "awesome",
"background_color": "#2196F3",
"icons": [
"src": "app-icon-4x.png",
"type": "image/png",
"sizes": "192x192"
],
"start_url": "index.html?launcher=true"
}
/* manifest.json */
{
"name": "your-awesome-app",
"short_name": "awesome",
"background_color": "#2196F3",
"icons": [
"src": "app-icon-4x.png",
"type": "image/png",
"sizes": "192x192"
],
"start_url": "index.html?launcher=true"
}
standalone window
/* manifest.json */
{
"name": "your-awesome-app",
"short_name": "awesome",
"background_color": "#2196F3",
"icons": [
"src": "app-icon-4x.png",
"type": "image/png",
"sizes": "192x192"
],
"start_url": "index.html"
}
/* manifest.json */
{
"name": "your-awesome-app",
"short_name": "awesome",
"background_color": "#2196F3",
"icons": [
"src": "app-icon-4x.png",
"type": "image/png",
"sizes": "192x192"
],
"start_url": "index.html?launcher=true"
}
/* manifest.json */
{
"name": "your-awesome-app",
"short_name": "awesome",
"background_color": "#2196F3",
"icons": [
"src": "app-icon-4x.png",
"type": "image/png",
"sizes": "192x192"
],
"start_url": "index.html?launcher=true"
}
Add to homescreen
self.addEventListener('beforeinstallprompt', e => {
})
self.addEventListener('beforeinstallprompt', e => {
e.userChoice.then(choice => {
// send choice to analytics
})
})
self.addEventListener('beforeinstallprompt', e => {
e.preventDefault()
})
let deferred
self.addEventListener('beforeinstallprompt', e => {
e.preventDefault()
deferred = e
})
let deferred
self.addEventListener('beforeinstallprompt', e => {
e.preventDefault()
deferred = e
})
deferred.prompt()
fast/smooth
works offline
lives on your phone
native functionalities
Notification.permission
if (Notification.permission === 'default') {
Notification.requestPermission(permission => {
if (permission === 'granted') {
new Notification('Hi!')
}
})
} else if (Notification.permission === 'granted') {
new Notification('Welcome back!')
} else {
// can't do anything!
}
if (Notification.permission === 'default') {
Notification.requestPermission(permission => {
if (permission === 'granted') {
new Notification('Hi!')
}
})
} else if (Notification.permission === 'granted') {
new Notification('Welcome back!')
} else {
// can't do anything!
}
if (Notification.permission === 'default') {
Notification.requestPermission(permission => {
if (permission === 'granted') {
new Notification('Hi!')
}
})
} else if (Notification.permission === 'granted') {
new Notification('Welcome back!')
} else {
// can't do anything!
}
if (Notification.permission === 'default') {
Notification.requestPermission(permission => {
if (permission === 'granted') {
new Notification('Hi!')
}
})
} else if (Notification.permission === 'granted') {
new Notification('Welcome back!')
} else {
// can't do anything!
}
if (Notification.permission === 'default') {
Notification.requestPermission(permission => {
if (permission === 'granted') {
new Notification('Hi!')
}
})
} else if (Notification.permission === 'granted') {
new Notification('Welcome back!')
} else {
// can't do anything!
}
if (Notification.permission === 'default') {
Notification.requestPermission(permission => {
if (permission === 'granted') {
new Notification('Hi!')
}
})
} else if (Notification.permission === 'granted') {
new Notification('Welcome back!')
} else {
// can't do anything!
}
if (Notification.permission === 'default') {
Notification.requestPermission(permission => {
if (permission === 'granted') {
new Notification('Hi!')
}
})
} else if (Notification.permission === 'granted') {
new Notification('Welcome back!')
} else {
// can't do anything!
}
navigation.geolocation
navigator.geolocation.getCurrentPosition(position => {
})
navigator.geolocation.getCurrentPosition(position => {
// position.coords.latitude
// position.coords.longitude
})
navigator.mediaDevices
upload files
bluetooth
nfc
record audio
vibration
battery state
offline storage
orientation
clipboard
notifications
geolocation
camera
background sync
speech recognition
fast/smooth
works offline
lives on your phone
native functionalities
web vs native
it depends™
games/tools
utility/daily use
email chat notes news social travel
utility/daily use
email chat notes news social travel
exception: twitter/uber
transaction
ecommerce appointments dining
transaction
ecommerce appointments dining
exception: iOS
both?
sharing code
sharing code
one language across the stack
common logic + different views
promise of the mobile web
@siddharthkpDM is open, say hi!
@greatindiandev bit.ly/gidslinkedin www.developersummit.com
Conference and Deep Dive Sessions
April 24-28, IISc Bangalore
TM
2018
Register early and get the best discounts!