advanced i/o in browser

112
Advanced I/O in browser Eugene Lazutkin, 5/2/2017, ClubAjax , Dallas, TX @uhop , www.lazutkin.com

Upload: eugene-lazutkin

Post on 29-Jan-2018

222 views

Category:

Technology


0 download

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.

The ideal

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 V

We interpret MIME.If you don’t set:

Content-TypeAccept-Type

⇒ application/json

Design decisions VI

Recognize special data:FormData — forms.Document — XML.ArrayBuffer, Blob.

Design decisions VII

Form query:Bad:url: '/abc/?name=' + name + '&age=' + age

Good:url: '/abc', query: {name: name, age: age}

XKCD: Exploits of a Mom

https://xkcd.com/327/Don’t be a Bobby Tables!

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?

What we have

XHR: history

The venerable XMLHttpRequest.Implemented by Microsoft.

For Outlook Web Access.Shipped in March 1999 in IE5.

As an ActiveX.

Oh, 1999!

March 1999

The Matrix was released.It blew my mind!

You go, bros sisters!

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.

Oh, 2005!

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.

Libraries to therescue!

Big gorillas: Angular/React

Both are soft on I/O and data.Variety of possible options.

Good for us?

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

So how to POST JSON?

Summary

We can use whatever.Including our ideal API.Let’s go back to our micro-library.

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); } });

Advanced I/OFinally!

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.

Extensibility Flexibility

Orchestration on top of the idealAPI.

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: bust service III

By default: no bust.Con�gurable:

Bust key.Bust value generating.

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: mock service VIII

// server error io.mock('/abc', () => io.mock.makeXHR({status: 500}) );

heya/io: bundle service I

Traditional I/OClient Server

HT

TP

con

nect

ions

HT

TP

connections

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 VII

Bundle’s narrative:Client unbundles responses.Noti�es requesters.

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 I

Typical waterfall (served locally):

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 IV

Prefetch (served locally):

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 VI

Typical waterfall (mobile):

heya/io: prefetch VII

Prefetch (mobile):

heya/io: prefetch VII

It was 16.88s, now it is 15.36s.We saved 1.52s!We deducted almost all /api.

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.

That’s all,folks!