advanced i/o in browser
TRANSCRIPT
Advanced I/Oin browser
Eugene Lazutkin, 5/2/2017, ClubAjax, Dallas, TX @uhop, www.lazutkin.com
Outline
Prototype an I/O library.What we have,
and what is wrong with it.Libraries!
What do Angular and React do?Back to the board!Advanced I/O
Browser
Now provides all low-level stuff.We can (without jQuery!):
Search by selector.Calculate geometry.Manipulate CSS classes.Much more!
How about I/O?
Stone age.We still need to use libraries.Let’s design an ideal solution.Then compare it with what we have.
HTTP
Simple text format.The same is used by HTTPS.Covered by standards.Uses verbs and status codes.Consists of headers and body.
HTTP request
POST /api HTTP/1.1 Host: localhost:8000 Connection: keep-alive User-Agent: ...user agent string... Content-Type: application/json Accept-Encoding: gzip, deflate Cookie: io=JSwSxqG0tIMQ_ZtZAABl
...body, if needed...
HTTP response
HTTP/1.1 200 OK Content-Type: application/json Content-Length: 18193 Date: Sun, 30 Apr 2017 23:52:36 GMT
...body, if needed...
Design decisions I
Return Promise.Helps with composability.A way from the callback hell.
Pack all request parameters together.Use raw {} for simplicity.We can manipulate it.
Design decisions II
Be smart about responseUsers want to deal in terms of data:
Send dataReceive data
In most cases a response object is notneeded.
Design decisions III
Be smart about responseSometimes we need a response:
To read status and headers.Cater for most common cases:
JSON in/out should be simple.The rest should be possible.
Design decisions IV
We interpret status code.4XX and 5XX are bad statuses.204 is good, but no response.
⇒ undefined.
Design decisions VII
Form query:Bad:url: '/abc/?name=' + name + '&age=' + age
Good:url: '/abc', query: {name: name, age: age}
Design decisions VIII
Different types for different errors.Use instanceof to make a choice.Errors: wrong request, timeout, badstatus.
Result
io({ url: url, method: 'POST', headers: { 'X-Custom-Header': 'abc' }, data: data // JSON }) // then() and catch()
Result: POST JSON example
io(req).then(result => { console.log(result.name); }).catch(res => { if(res instanceof io.FailedIO) {} if(res instanceof io.TimedOut) {} if(res instanceof io.BadStatus) { console.log(res.xhr.status); } });
How we interpret result?
We know Content-Type.application/json ⇒ JSON.application/xml ⇒ XML.
MIME types are covered by RFCs.We can map them to whatever.Or return a string or a buffer.
Design: helpers I
We can provide helpers for verbs:io.get (url, queryData); io.head (url, queryData); io.post (url, data); io.put (url, data); io.patch (url, data); io.delete (url, data);
Design: helpers II
url is a string, or an object like for io().queryData and data are optional.A helper overrides a verb.
Design: helpers III
// url as an object? for REST: var opts = {url: url, headers: headers}; io.get(opts); io.put(opts, data); io.patch(opts, data); io.delete(opts);
POST JSON reformulated
io.post(url, data). then(result => ...). catch(res => ...);
Sounds simple, eh?
XHR: history
The venerable XMLHttpRequest.Implemented by Microsoft.
For Outlook Web Access.Shipped in March 1999 in IE5.
As an ActiveX.
Let’s POST JSON with XHR I
var xhr = new XMLHttpRequest(); xhr.open('POST', url, true); // set up headers xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Accept-Type', 'application/json'); // continues on the next slide
Let’s POST JSON with XHR II
// optional xhr.overrideMimeType( 'application/json'); // set up callbacks xhr.onload = function (e) {}; xhr.onerror = function (e) {}; xhr.ontimeout = function (e) {}; // continues on the next slide
Let’s POST JSON with XHR III
// optional xhr.onprogress = function (e) {}; xhr.upload.onprogress = function (e) {}; // finally xhr.send(JSON.stringify(data));
XHR: summary
Three slides of boilerplate.Much more with actual code.Callbacks are not composable.A lot of repetitive code.Realistically requires a wrapper.
JSONP: history
Formulated by Bob Ippolito.Published in December 2005.Uses existing facilities: <script>Works in all browsers.Works cross-origin.
December 2005
First Narnia came out.SNL’s “Lazy Sunday” on selecting “thedopest route” to see it:
– I prefer MapQuest! – That’s a good one too! – Google Maps is the best! – True dat! Double true!
Let’s POST JSON with JSONP
We can’t. Limitations:Only GET verb.Only query parameters.Only JSON as a returned value.
Let’s GET JSON with JSONP I
var script = document.createElement('script'); script.onerror = function (e) {}; window.unique = function (data) { delete window.unique; script.parentNode.removeChild(script); // do something with data }; // continues on the next slide
Let’s GET JSON with JSONP II
// now we run it script.src = url + /* our parameters */ '?a=1' + '&callback=' + encodeURIComponent('unique'); document.documentElement. appendChild(script);
JSONP: summary
Let’s face it: it is a hack.Callbacks are not composable.Repetitive code to encode parameters.Realistically requires a wrapper.
Fetch: history
Started by WHATWG.Status (April 2017):
Not available in IE11.Available everywhere else.
Reasonable poly�ll is available.
Fetch: details
Uses promises (�nally!).Model:fetch(req).then(res => ...)
Allows to handle redirects, CORS.
Fetch: more details
Simpli�es Service Workers.Currently can’t be canceled.Supports streaming.De�nes numerous classes.Can be heavy when poly�lled.
Fetch: even more details
No cookies are sent by default.CORS is disabled by default.Any response from a server is success.
Even 4XX and 5XX ones.
Let’s POST JSON with Fetch I
The example is adapted from ShahalTalmi’s article.
Angular core contributor.var opts = { method: 'POST', mode: 'cors', credentials: 'include' // cookies! }; // continues on the next slide
Let’s POST JSON with Fetch II
// continue filling in opts opts.headers = { 'Content-Type': 'application/json', 'Accept-Type': 'application/json' }; opts.body = JSON.stringify(data); // continues on the next slide
Let’s POST JSON with Fetch III
fetch(url, opts).then(response => response.ok ? response.json() : Promise.reject(response.status) ) // user's code for then() & catch()
Fetch: summary
Streaming is nice to have, but it is a rareedge case.Still a lot of repetitive code.
Due to selected defaults.Realistically requires a wrapper.
Angular 2/4 — don’t
Shahar Talmi (Angular core contributor)wrote:
Why I won’t be using Fetch API in myapps
It is a telling title.
Angular 2/4 — do
Shahar suggests:jQuery’s $.ajax()Angular’s $httpSuperAgentAxios — his favorite
Angular’s $http I
Main:$http(req).toPromise(). then(res => ...)
De�nes helpers: get(), head(),post(), put(), delete(), jsonp(),patch()
Angular’s $http II
Supports transforming requests andresponses.Supports GET and JSONP caching.Supports setting defaults.
Angular’s $http: example
this.http.post(url, data).toPromise(). then(res => { console.log(res.data); }).catch(res => { console.log(res.status); });
SuperAgent
request .post('/api/pet') .send({name: 'Manny'}) .set('Accept', 'application/json') .end((err, res) => alert(err || !res.ok ? 'Oh no! error' : JSON.parse(res.body)) });
Axios I
POST JSON using the main method:axios({method: 'post', url: url, data: data}) // user's then() & catch()
Axios II
POST JSON using a helper:axios.post(url, data).then(res => { console.log(res.data); }).catch(res => { console.log(res.status); });
React — do
ReduxSimple: reducers, actions,middlewares, and stores.
RelayImplement GraphQL on serversMore modern: Relay Modern
React — really???
Answered on StackOver�ow:Fetch API with poly�llSuperAgentAxios
heya/io I
https://github.com/heya/ioBuilt using our ideal API.Supports pluggable services.Supports pluggable transports.Thoroughly battle-tested.
heya/io II
Can be used with AMD and globals.Works with native Promise or anyPromise-like object.Can be used with heya/async.
Can cancel() I/O.Supports progress.
POST JSON with heya/io
io.post(url, data).then(result => { console.log(result); }).catch(res => { if (res instanceof io.BadStatus) { console.log(res.xhr.status); } else { console.log(res); } });
Orchestrating I/O
I/O is much more than just a transport.Caching is a big topic.
App-level cache.Cache busting.
I/O testing is a must.
Cache issues
In HTTP world cache:Controlled by a server.Server cannot recall a response.Client cannot evict a response.
App-level cache
App frequently knows:When an object is modi�ed.Dependencies between objects.
We need an app-level cache.The alternative: no HTTP cache.
Cache issues: servers
Server cache headers are frequentlymiscon�gured.Evidence: cache-busting./url?bust=123456789
bust uses a random payload, so allrequests are unique.
heya/io is extendable I
The I/O pipeline is well-de�ned.All stages can be extended:
On per-request basis.On per-app basis.
heya/io is extendable II
Pipeline extensions: services.XHR replacements: transports.All extensions should be includedexplicitly.
Pay only for what you use.
heya/io: bust service I
var req = io.get({ url: 'abc', bust: true }); // abc?io-bust=1470185125354-507943
heya/io: bust service II
var req = io.get({ url: 'abc', bust: 'xyz' }); // abc?xyz=1470185125354-507943
heya.io: cache service I
Storage-based:Session storage (default).Local storage (permanent).
Caches GET requests automatically.To opt out:cache: false
heya.io: cache service II
Main API (rarely used directly):io.cache.save('/abc', {a: 1}); io.cache.remove('/abc');
Direct access to the underlying storageobject.
heya/io: mock service I
Simple way to intercept, replace, ortransform an I/O request.A must for testing!Trivial redirects.Rapid prototyping.
heya/io: mock service II
// canned data for exact url io.mock('/abc', () => 42); io.get('/abc').then(data => { console.log(data); // 42 });
heya/io: mock service III
// canned data for url prefix io.mock('/abc*', () => 42); io.get('/abc/1').then(data => { console.log(data); // 42 });
heya/io: mock service IV
// redirect io.mock('/abc', () => io.get('/xyz')); io.get('/abc').then(data => { console.log(data); // from /xyz });
heya/io: mock service V
// timeout (uses heya/async) io.mock('/abc', () => timeout.resolve(500).then(() => 42)); io.get('/abc').then(data => { console.log(data); // 42 after 0.5s });
heya/io: mock service VI
// timeout (uses setTimeout()) io.mock('/abc', () => new Promise(resolve => { setTimeout(function () { resolve(42); }, 500); })); io.get('/abc').then(data => { console.log(data); // 42 after 0.5s });
heya/io: mock service VII
// cascaded calls io.mock('/abc', () => io.get('/a').then( value => io.get('/b', {q: value.x}) ).then( value => io.get('/c', {q: value.y}) ) );
heya/io: bundle service II
Problems with the tradition:We may exceed number of HTTPconnections.
Potential stalling.
heya/io: bundle service III
Problems with the tradition:Each payload is small, andcompressed separately.
Poor compression.
heya/io: bundle service IV
Bundle I/OClient Server
HT
TP
con
nect
ions
HT
TP
connections
bundle bundle
heya/io: bundle service V
Bundle’s narrative:Client collects I/O requests.Bundles them in one request.Sends it to a well-known URL.
heya/io: bundle service VI
Bundle’s narrative:Server acts as a proxy.Runs all requests in parallel locally.Sends back collected responses.
heya/io: bundle service VIII
Important points:Bundling works transparently.No code modi�cations!Usually GETs are bundled.To opt out:bundle: false
heya/io: bundle service IX
Assumes a server handler.Reference: heya/bundler.Local server connections are fast and low-lag.
heya/io: bundle service X
Bundling helps with compression.Bundling requires just one HTTPconnection.
heya/io: bundle service XI
HTTP/2 alleviates some problems.Bundle as fast as the slowest request.In a real app the speed gain was up to30%.
heya/io: bundle service XII
Bundler can return responses forunrequested requests.
Similar to HTTP/2 Server Push.Cache will be populated.
heya/io: bundle service XIII
Behind the scenes:Client collects requests.Sends the array as JSON.Server unpacks.Runs them in parallel.
heya/io: bundle service XIV
Behind the scenes:Server collects responses.
Including errors.Sends the array as JSON back.Client unpacks, saves to cache.
heya/io: prefetch II
What if we call /api before loadinganything?Prefetch AKA data forward.The trick is to do it without libraries.
heya/io: prefetch III
//<html><head><script> (function () { var xhr = new XMLHttpRequest(); xhr.onload = function () { window._r = JSON.parse(xhr.responseText); }; xhr.open('POST', '/api', true); xhr.send(null); }()); //</script></head></html>
heya/io: prefetch V
It was 3.69s, now it is 3.22s.We saved 470ms — whoopee do!It was under ideal conditions.Really fast 12 core 64G RAM rig.How about mobile users?
heya/io: prefetch VIII
bundle service has provisions forprefetching.It can be done transparently!See “Cookbook: bundle” in Wiki ofhttps://github.com/heya/io
heya/io: summary
Micro-library.All code < 5k (min/gzip).No hard dependencies!
Simple ideal API.Battle-tested and proven.Works with all libraries.
Summary
Correctly using I/O we can:Greatly improve performance.Write clear clean code.Use ideal API.Improve perf for free.