Let

the main thread breathe

Majid Hajian

Around 90% of all internet users have mobile access

mhadaily

mhadaily

mhadaily

mhadaily

mhadaily

WE NEED TO CARE ABOUT ALL OF OUR USERS

mhadaily

What will be explored

Web Workers
Web Assembly

mhadaily

Worklets

<ME />

import HelloWorld from "@/components/HelloWorld.component";
export default {
  name: "whoAmI",
  data: {
      me: {
        name: "Majid Hajian",
        location: "Oslo, Norway",
        description: ```
        	Passionate Software engineer, 
	        Community Leader, Author and international Speaker
         ```,
        main: "Web, Javascripter, Flutter/Dart, IoT",
        homepage: "https://www.majidhajian.com",
        socials: {
          twitter: "https://www.twitter.com/mhadaily",
          github: "https://www.github.com/mhadaily"
        },
        books: {
          "Progressive Web App with Angular": {
             version: "1.0.0", 
             publishedBy: "Apress", 
             foundOn: "www.pwawithangular.com",
          }
        },
        author: {
          packtpub: "PWA development, 7 hours video course",
          Udemy: "PWA development, 7 hours video course"
        }
        founder: "Softiware As (www.Softiware.com)"
        devDependencies: {
          tea: "green", mac: "10.14+",
        },
        engines: {
          FlutterVikings: "Orginizer", MobileEraConference: "Orginizer",
          ngVikingsConference: "Orginizer", 
          MobileMeetupOslo:"Co-Orginizer",AngularOslo: "Co-Orginizer",
          FlutterDartOslo: "Co-Orginizer",Framsia: "Co-Orginizer",          
        }};} 
     };

mhadaily

Find me on the internet by

cc: medium.com/@francesco_rizzi

  • Events
  • Javascript
  • Style
  • Layout
  • Paint
  • Compositing

mhadaily

60 

FPS

mhadaily

Budget

60 frames into one second

~=

16.6 milliseconds

1 / 60 = 0.0166666

mhadaily

16 milliseconds

iPhone X

Moto G4

Nokia 2.1

Same tasks!

16

mhadaily

mhadaily

unpredictability

We as developer 

must handle unexpected to bring better user experience to our users in another word

we should let main thread breathe by giving it enough space and air!

mhadaily

Grand Central Dispatch

DispatchQueue in Swift 

An object that manages the execution of tasks serially or concurrently on your app's main thread or on a background thread.

let label = UILabel();

DispatchQueue.global(qos: .background).async {
    // do your job here

    DispatchQueue.main.async {
        // update ui here
    }
}

So executing a task on a background queue and updating the UI on the main queue after the task finished is a pretty easy one using Dispatch queues.

mhadaily

AsyncTask

Android

Example

mhadaily

But...

Javascript

runs on single thread

Note: To do parallelism with Javascript check out my full talk on it 

mhadaily

Web Workers

  • They are isolated 
  • no variable can be shared

headless

main browser

mhadaily

  • They run in parallel 
  • DOM is not available 
const worker = new Worker("./worker.js");
let counter = 0;
setInterval(() => {
  counter = counter + 1;
  worker.postMessage({ action: "power2", payload: counter });
}, 3000);
self.onmessage = message => {
  const { data } = message;
  switch (data.action) {
    case "power2":
      const number = data.payload; const power2 = number * number;
      self.postMessage({ command: "power2", payload: power2 });
      break;
  }};

Worker Thread

function  power2(){}

const w = new Worker();

mhadaily

mhadaily

w.power2(5);

Main Thread

mhadaily

// Main 
import * as Comlink from "https://unpkg.com/comlink@alpha/dist/esm/comlink.mjs";
// import * as Comlink from "../../../dist/esm/comlink.mjs";

async function init() {
  const worker = new Worker("./worker.js");
  const service = Comlink.wrap(worker);
  const power2 = await service.power2(2);
  console.log(power2);
  const increment = await service.increment(2);
  console.log(increment);
}
init();
// Worker
importScripts("https://unpkg.com/comlink@alpha/dist/umd/comlink.js");
// importScripts("../../../dist/umd/comlink.js");

const service = {
  power2: value => value * value,
  increment: (value) => value * 1
};

Comlink.expose(service);
// all imports 
class UsersWrapper extends Component {
    worker;
    workerService;
  
    constructor(props) {
        super(props);
        this.state = {
            users: [],
        };
    }
    componentDidMount() {
        this.worker = new Worker('./worker.js');
      
        this.workerService = Comlink.wrap(worker);
      
        // Assumption is we have a list of 3000 users! 
        fetchUsers().then(users => {
            this.setState({ users });
        })
    }
    async sortAscending() {
        const sortedUsers = await this.workerService.sort(this.state.users);
		this.setState({ users: sortedUsers });
        return;
    }
   componentDidMount(){
     this.worker.terminate();
   }
   render() {
      return this.state.users.slice(0,20).map((user, index) => (<User key={user.id}>);
   }
}

our business  logic to worker thread 

UI logic to main thread

mhadaily

View

Actions

Reducer

Store

Might be even blocking

mhadaily

View

Actions

Reducer

Store

Web Worker

Main Thread

mhadaily

const initialState = { count: 0 };

const delayFunction = () => {
  console.log('Start to delay...');
  const seconds = 3;
  const start = new Date().getTime();
  const delay = seconds * 1000;
  while (true) {
    if (new Date().getTime() - start > delay) {
      break;
    }
  }
  console.log('Finished delaying');
};

export default (state = initialState, action) => {
  switch (action.type) {
    case 'increment':
      delayFunction();
      return { ...state, count: state.count + 1 };
    case 'decrement':
      delayFunction();
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
};

REDUCER

import { expose } from 'comlink';
import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(reducer);

expose(store);

store.worker.js


const withOutWebWorker = async () => {
  const store = createStore(reducer);
  ReactDOM.render(<App store={store} />, document.getElementById('app'));
};

const withWebWorker = async () => {
  const worker = new Worker('./store.worker.js');
  const remoteStore = wrap(worker);
  const store = await remoteStoreWrapper(remoteStore);

  ReactDOM.render(<App store={store} />, document.getElementById('app2'));
};

app.js

Does it even faster?

UI Thread

mhadaily

UI Thread

Worker Thread

RELIABLE

Sometimes it better keep the user waiting a bit longer instead of making unresponsive UI or block ui

mhadaily

mhadaily

Web Assembly

mhadaily

AssemblyScript compiles a strict subset of TypeScript to WebAssembly 

npm init
npm install assemblyscript/assemblyscript
npx asinit .


/*
  ./assembly
  Directory holding the AssemblyScript sources being compiled to WebAssembly.

  ./assembly/tsconfig.json
  TypeScript configuration inheriting recommended AssemblyScript settings.

  ./assembly/index.ts
  Exemplary entry file being compiled to WebAssembly to get you started.

  ./build
  Build artifact directory where compiled WebAssembly files are stored.

  ./build/.gitignore
  Git configuration that excludes compiled binaries from source control.

  ./index.js
  Main file loading the WebAssembly module and exporting its exports.

  ./package.json
  Package info containing the necessary commands to compile to WebAssembly.
*/

mhadaily

// no optimization, no memoization
export function fibonacci(num: i32): i32 {
  if (num <= 1) return 1;

  return fibonacci(num - 1) + fibonacci(num - 2);
}
/** Creates a new array and returns it to JavaScript. */
export function createArray(length: i32): Int32Array {
  return new Int32Array(length)
}
/** Randomizes the specified array's values. */
export function randomizeArray(arr: Int32Array): void {
  for (let i = 0, k = arr.length; i < k; ++i) {
    let value = i32((Math.random() * 2.0 - 1.0) * i32.MAX_VALUE)
    unchecked(arr[i] = value)
  }
}
/** Computes the sum of an array's values and returns the sum to JavaScript. */
export function sumArray(arr: Int32Array): i32 {
  let total = 0
  for (let i = 0, k = arr.length; i < k; ++i) {
    total += unchecked(arr[i])
  }
  return total
}
// We'll need the unique Int32Array id when allocating one in JavaScript
export const Int32Array_ID = idof<Int32Array>()
// no optimization, no memoization
export function fibonacci(num: i32): i32 {
  if (num <= 1) return 1;

  return fibonacci(num - 1) + fibonacci(num - 2);
}
/** Creates a new array and returns it to JavaScript. */
export function createArray(length: i32): Int32Array {
  return new Int32Array(length)
}
/** Randomizes the specified array's values. */
export function randomizeArray(arr: Int32Array): void {
  for (let i = 0, k = arr.length; i < k; ++i) {
    let value = i32((Math.random() * 2.0 - 1.0) * i32.MAX_VALUE)
    unchecked(arr[i] = value)
  }
}
/** Computes the sum of an array's values and returns the sum to JavaScript. */
export function sumArray(arr: Int32Array): i32 {
  let total = 0
  for (let i = 0, k = arr.length; i < k; ++i) {
    total += unchecked(arr[i])
  }
  return total
}
// We'll need the unique Int32Array id when allocating one in JavaScript
export const Int32Array_ID = idof<Int32Array>()
var int8: i8 = <i8>0; // 8-bit signed integer [-128 to 127]
var uint8: u8 = <u8>0; // 8-bit unsigned integer [0 to 255]
var int16: i16 = <i16>0; // 16-bit signed integer [-32,768 to 32,767]
var uint16: u16 = <u16>0; // 16-bit unsigned integer [0 to 65,535]
var int32: i32 = <i32>0; // 32-bit signed integer [-2,147,483,648 to 2,147,483,647]
var uint32: u32 = <u32>0; // 32-bit unsigned integer [0 to 4,294,967,295]
var int64: i64 = <i64>; // 64-bit signed integer [-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807]
var uint64: i64 = <u64>0; // 64-bit unsigned integer [0 to 18,446,744,073,709,551,615]
var float32: f32 = <f32>0.0; // 32-bit float [32 bit float range]
var float64: f64 = <f64>0.0; // 64-bit float [64 bit float range]
var pointer: usize = <usize>0; // a 32/64-bit pointer to a location in memory
npm run asbuild


/**
Created: ./build/optimized.wasm
Created: ./build/optimized.wasm.map
Created: ./build/optimized.wat
Created: ./build/untouched.wasm
Created: ./build/untouched.wasm.map
Created: ./build/untouched.wat
 * /

// untouched.wasm is good for debugging
// optimized.wasm can be used in production
// wat is the text representation of the genrated .wasm 

mhadaily

// all other imports 
import { instantiateStreaming, ASUtil } from "assemblyscript/lib/loader";

// no optimization, no memoization
function fibonacci(num) {
  if (num <= 1) return 1;

  return fibonacci(num - 1) + fibonacci(num - 2);
}


class FibWrapper extends Component {
    imports = {};
	wasmService;

  async componentDidMount() {
    this.wasmService = await instantiateStreaming(fetch('/optimized.wasm'), this.imports);
  }

  async factorialwasm() {
    const result = await this.wasmService.fibonacci(9999);
    this.setState({ number: result });
    return;
  }

  factorialJS() {
    const result = fibonacci(9999);
    this.setState({ number: result });
    return;
  }
   render() {
      return <Fib />;
   }
}

mhadaily

MEASURE

mhadaily

Worklets

A lightweight version of WebWorkers

Access to low-level parts of rendering pipleline

Run Javascript and WebAssembly

graphic rendering or audio processing

High performance

mhadaily

 Worklets Types

Paint

Animation

Layout

Audio

mhadaily

CSS Paint API

 

(also known as “CSS Custom Paint” or “Houdini’s paint worklet”)

mhadaily

// checkerboard.js
class CheckerboardPainter {
    paint(ctx, geom, properties) {
      // Use `ctx` as if it was a normal canvas
      const colors = ['black', 'white'];
      const size = 32;
      for(let y = 0; y < geom.height/size; y++) {
        for(let x = 0; x < geom.width/size; x++) {
          const color = colors[(x + y) % colors.length];
          ctx.beginPath();
          ctx.fillStyle = color;
          ctx.rect(x * size, y * size, size, size);
          ctx.fill();
        }
      }
    }
  }

// Register our class under a specific name
registerPaint('checkerboard', CheckerboardPainter);

mhadaily

<!-- index.html -->
<!doctype html>
<style>
  textarea {
    background-image: paint(checkerboard);
  }
</style>
<textarea></textarea>
<script>
  if (typeof CSS === 'undefined' || !('paintWorklet' in CSS)) {
    return;
  } else {
	  CSS.paintWorklet.addModule('checkerboard.js');
  }
</script>

mhadaily

mhadaily

mhadaily

Demo

source code

https://github.com/mhadaily/off-main-thread-examples

Let's embrace the power of workers

let's make our web app more reliable & usable

Majid Hajian

mhadaily

Slides and link to source code

bit.ly/let-the-main-thread-breathe

majid[at]softiware[dot]com

Let the main thread breathe

By Majid Hajian

Let the main thread breathe

The main thread, on the web, has a lot of responsibilities. At the same time, web apps are getting more sophisticated every day. Therefore, the main thread gets too busy that will disappoint our user by showing janky frames! The off-main-thread architecture ensures apps run smoothly on every device for everyone. In this talk, we will go through the possibilities in browsers such as WebWorker, Worklet, and WebAssembly by introducing practical tools that allow us to boost our user experiences.

  • 603

More from Majid Hajian