Andrea Coiutti
Front-end Developer. UX/UI Engineer. Webdesigner.
Andrea Coiutti (@andreacoiutti)
Universal JS Day - Ferrara, 20/04/18
SPEAKER
SPEAKER
AMPLIFIER
DAC
DIGITAL SOURCE
WEB RADIO
NAS
USB DRIVE
SPEAKER
SPEAKER
AMPLIFIER
DAC
DIGITAL SOURCE
sending the audio file unaltered to the audio device
no DSP (volume control, sample rate conversion, dither)
1
0
1
1
0
1
0
0
0
1
1
WEB RADIO
NAS
USB DRIVE
SPEAKER
SPEAKER
AMPLIFIER
DAC
DIGITAL SOURCE
CLIENT
Wife Acceptance Factor
INEXPENSIVE
INEXPENSIVE
SMALL
INEXPENSIVE
SMALL
SILENT
INEXPENSIVE
SMALL
SILENT
GREEN
INEXPENSIVE
SMALL
SILENT
GREEN
HEADLESS
WEB RADIO
NAS
USB DRIVE
SPEAKER
SPEAKER
AMPLIFIER
DAC
DIGITAL SOURCE
CLIENT
+
"Any application that can be written in JavaScript, will eventually be written in JavaScript."
- Atwood's Law
A modern client-side JavaScript framework
for building Single Page Applications
Virtual DOM, components, batteries included
Vue + Vue-Router + Vuex + fetch (40kb)
React + React-Router + Redux + fetch (64kb)
Angular (135kb)
Mithril (8kb)
(source: mithril.js.org)
Vue (9.8ms)
React (12.1ms)
Angular (11.5ms)
Mithril (6.4ms)
first render time, less is better (source: mithril.js.org)
Thousands of entries in queue
viewport
real entries in DOM
offset from top
viewport
real entries in DOM
offset from top
scrolled distance
viewport
real entries in DOM
offset from top
Tons of coverart pictures
defer loading of non-critical resources at page load time,
usually using event handlers (scroll or resize)
document.addEventListener('DOMContentLoaded', () => {
const lazyImages = [].slice.call(document.querySelectorAll('img.lazy'))
const lazyImageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const lazyImage = entry.target
lazyImage.src = lazyImage.dataset.src
lazyImage.classList.remove('lazy')
lazyImageObserver.unobserve(lazyImage)
}
});
});
lazyImages.forEach((lazyImage) => {
lazyImageObserver.observe(lazyImage)
});
});
<img class="lazy" src="placeholder-image.jpg" data-src="real-image.jpg">
(source: caniuse.com)
not fully supported yet, polyfill is needed
1. Install polyfill
npm install intersection-observer
import 'intersection-observer'
2. Use polyfill
filesystem
database
network
process
other
register
callback
operation
complete
trigger
callback
(single thread)
EVENT LOOP
requests
(event queue)
thread pool
Express
socket.io
Fast and low overhead web framework, for Node.js
brought by Matteo Collina (@matteocollina)
and Tomas Della Vedova (@delvedor)
(source: fastify.io)
const fastify = require('fastify')()
const serveStatic = require('fastify-static')
const { publicRoutes, routes } = require('./api/routes.js')
// Enable CORS - Cross Origin Resource Sharing
fastify.use(cors())
// Public routes
fastify.register(publicRoutes, { prefix: '/api' })
// All the other routes
fastify.register(routes, { prefix: '/api' })
// Static content
fastify.register(serveStatic, { root: '/opt/frontend/dist' })
// Start server
fastify.listen(80, '0.0.0.0', err => {
if (err) throw err
console.log(`Server listening on port 80`)
})
const apiAuth = require('../auth') // auth hook
const audio = require('./methods/audio.js')
const auth = require('./methods/auth.js')
// ...
const publicRoutes = (fastify, options, next) => {
fastify.post('/auth/login', auth.login)
fastify.post('/auth/reset-password', auth.resetPassword)
// ...
next()
}
const routes = (fastify, options, next) => {
// Add hook on this set of routes
fastify.addHook('preHandler', apiAuth.check)
fastify.get('/audio/cards', audio.getCards)
fastify.get('/audio/config', audio.getConfig)
// ...
next()
}
const auth = require('./methods/auth.js')
const joi = require('joi')
const validate = rule => ({
schema: rule,
schemaCompiler: schema => data => joi.validate(data, schema)
})
const loginSchema = {
body: joi
.object()
.keys({
email: joi.string().email().required()
password: joi.string().required()
})
.required()
}
fastify.post('/auth/login', validate(loginSchema), auth.login)
fastify.setNotFoundHandler((req, res) => {
res.code(404)
.type('application/json')
.send({
statusCode: 404,
error: 'Not Found',
message: 'Not found'
})
})
fastify.setErrorHandler((error, req, res) => {
if (error.isJoi) {
res.code(400)
.type('application/json')
.send({
statusCode: 400,
error: 'Bad Request',
message: error.message
})
} else { /* 500 Server Error */ }
})
request
content
available
in cache?
serve it
from cache
fetch or compute it
store and cache it
request
content
available
in cache?
serve it
from cache
fetch or compute it
store and cache it
request
content
available
in cache?
serve it
from cache
fetch or compute it
store and cache it
add job
operation
complete
"A simple, fast, robust job/task queue
for Node.js, backed by Redis"
<html>
<body>
<script src="./index.js"></script>
</body>
</html>
// import another component
import main from './main'
main()
// import a CSS module
import classes from './main.css'
export default () => {
console.log(classes.main)
}
.main {
/* Reference an image file */
background: url('./img/bg.png');
color: red;
}
index.html
index.js
main.js
main.css
> parcel index.html
Webpack (20.71s)
Parcel (9.98s)
Parcel with cache (2.64s)
Browserify (22.98s)
Bundle time, less is better (source: parceljs.org)
By Andrea Coiutti
In the last few months I've worked on the refactor of RuneAudio, an open source software that transforms inexpensive, silent and low-powered mini-PCs into hi-fi music players. The result (version 2.0) is a stack that speaks the same language: API + websocket in Node.js for the backend, SPA for the frontend. The work was also a playground to experiment with modern solutions and to push the performance to the limits: we'll touch topics as occlusion culling, lazy loading, caching, job queues, Mithril and Fastify.