Highlight
Safari 15 has made large-scale updates in three directions: JavaScript, WebAssembly and Web API, including private class fields, WeakRef, WebGL2, MediaRecorder and speech recognition API.
Core Content
Web developers have long faced a pain point on Safari: support for new features is always slow.WWDC2021 filled in a lot of gaps at once this time.
In terms of JavaScript engines, private class fields have finally been implemented.Previously prefixed with an underscore_startTimeIt’s just a convention, external code can still access it.Use now#startTime, encapsulation is enforced at the language level, and access will throw an error.
WeakRef and FinalizationRegistry make memory management more granular.You can hold a weak reference to the object without preventing garbage collection; you can also receive notifications to clean up the object when it is recycled.This is useful for caching systems and testing frameworks.
The WebAssembly engine has upgraded memory instructions, i64/BigInt interconversion, reference types, and streaming compilation.Compiling C++/Rust to the web is one step closer to performance.
In terms of Web API, WebGL2 is available on all platforms, MediaRecorder can record audio and video, Speech Recognition uses the same engine as Siri to convert speech to text, and Web Share supports sharing files.
Detailed Content
JavaScript private class fields
In the past, when writing classes, private members relied on convention:
class StopwatchWithOneButton {
constructor() {
this._startTime = null; // Only a convention; still accessible externally
}
click() {
if (this._startTime === null) {
this._startTime = Date.now();
} else {
const duration = Date.now() - this._startTime;
this._startTime = null;
return duration;
}
}
}
Use now#Prefix, language enforced private:
class StopwatchWithOneButton {
#startTime = null; // A true private field
click() {
if (this.#startTime === null) {
this.#startTime = Date.now();
} else {
const duration = Date.now() - this.#startTime;
this.#startTime = null;
return duration;
}
}
// Private methods
#start() { this.#startTime = Date.now(); }
#stop() {
const duration = Date.now() - this.#startTime;
this.#startTime = null;
return duration;
}
// Static private field
static #startedCount = 0;
// Public static field
static totalCreated = 0;
}
Key points:
#fieldIs language-level private, external access will throw TypeError (01:49)- Private methods are also used
#prefix, can only be called inside a class (03:15) - Support static private fields
#staticFieldand public static fieldsstaticField(03:39) - No need for WeakMap hack to implement private properties
WeakRef and FinalizationRegistry
Need to track a batch of objects but don’t want to prevent them from being garbage collected:
const allStopwatches = new Set();
const registry = new FinalizationRegistry((id) => {
// Automatically clean up when the object is collected
for (const ref of allStopwatches) {
if (ref.id === id) {
allStopwatches.delete(ref);
break;
}
}
});
function addStopwatch(sw) {
const id = generateId();
const ref = new WeakRef(sw);
ref.id = id;
allStopwatches.add(ref);
registry.register(sw, id); // Trigger the callback when sw is collected
}
function clickAllStopwatches() {
for (const ref of allStopwatches) {
const sw = ref.deref(); // Get the object; may return undefined
if (sw) sw.click();
}
}
Key points:
WeakRefHolding weak references, not preventing garbage collection (04:19)deref()Get the object, return if it has been recycledundefined(05:56)FinalizationRegistryTrigger callbacks to clean up objects when they are recycled (06:21)- The timing of garbage collection is uncertain, do not rely on it for key logic
Top-level await and Module Workers
Can be used directly in the moduleawait:
// stopwatch.mjs
const config = await fetch('/config.json').then(r => r.json());
export class Stopwatch { /* Use config */ }
// main.mjs
import { Stopwatch } from './stopwatch.mjs';
// Execution reaches here only after await in stopwatch.mjs completes
const sw = new Stopwatch();
Workers also support ES Modules:
// Create a module worker
const worker = new Worker('./worker.mjs', { type: 'module' });
// Service Worker
navigator.serviceWorker.register('./sw.mjs', { type: 'module' });
// Audio Worklet
await audioContext.audioWorklet.addModule('./processor.mjs');
Key points:
- top-level await is only available in ES Module, ordinary scripts will report syntax errors (07:37)
- Imported asynchronous modules will block the execution of modules that depend on it (08:38)
- module workers support dynamic import, optimized loading and dependency management (09:21)
- web workers, service workers, and worklets all support modules (09:51)
WebGL2 and WebM/VP9 support
New features in WebGL2:
- 3D textures, supporting volume effects such as clouds (19:03)
- Sampler object, more flexibility in using textures in shaders (19:09)
- Transform feedback, implementing particle systems on GPU (19:16)
- Backend migrated from OpenGL to Metal, iOS Simulator can also use GPU (20:23)
- Analyzing WebGL code with the Xcode Frame Debugger (20:40)
WebM and VP9:
- macOS 11.3+ supports VP8/VP9 video + Vorbis audio (21:19)
- macOS 12 adds Opus audio support (21:30)
- iPadOS 15 supports MSE streaming WebM (21:40)
- Use the MediaCapabilities API to detect specific support (21:47)
MediaRecorder records audio and video
let mediaRecorder;
let recordedChunks = [];
async function startRecording() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm',
audioBitsPerSecond: 128000
});
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) recordedChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: 'audio/webm' });
const url = URL.createObjectURL(blob);
document.querySelector('audio').src = url;
};
mediaRecorder.start();
}
function stopRecording() {
mediaRecorder.stop();
}
Key points:
MediaRecorderCapturing data from a media element or MediaStream (25:23)- Specifiable MIME type and bitrate (25:39)
ondataavailableCollect pieces of data,onstopAssembling into Blobs (26:52)- Supports recording from microphone, camera or screen
Audio Worklet real-time audio processing
// distortion-processor.mjs
class DistortionProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
for (let channel = 0; channel < input.length; channel++) {
const inputChannel = input[channel];
const outputChannel = output[channel];
for (let i = 0; i < inputChannel.length; i++) {
outputChannel[i] = this.distort(inputChannel[i]);
}
}
return true; // Keep active
}
distort(x) {
return x > 0 ? Math.min(x * 2, 1) : Math.max(x * 2, -1);
}
}
registerProcessor('distortion', DistortionProcessor);
// main.js
async function startRecordingWithEffect() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audioContext = new AudioContext();
await audioContext.audioWorklet.addModule('./distortion-processor.mjs');
const source = audioContext.createMediaStreamSource(stream);
const effectNode = new AudioWorkletNode(audioContext, 'distortion');
const destination = audioContext.createMediaStreamDestination();
source.connect(effectNode).connect(destination);
// Record the processed audio
mediaRecorder = new MediaRecorder(destination.stream);
mediaRecorder.start();
}
Key points:
- Audio Worklet runs in the audio rendering thread, reducing switching between the main thread and the rendering thread (27:37)
- Lower latency than old ScriptProcessorNode (27:49)
- Processors must inherit
AudioWorkletProcessorand implementprocess()Methods (29:01) - use
registerProcessor()Only after registration can it be passed in the main thread.AudioWorkletNodeUse (29:38)
Web Share Share files
async function shareAudio(blob, filename) {
const file = new File([blob], filename, { type: 'audio/webm' });
const shareData = {
files: [file],
title: 'Voice Memo',
text: 'Check out my recording'
};
if (navigator.canShare && navigator.canShare(shareData)) {
await navigator.share(shareData);
}
}
Key points:
- Web Share API adds file sharing support this year (30:32)
- Share pictures, videos, audio and other file types (30:36)
- call first
canShare()Check whether it is supported and then callnavigator.share()(31:37) - The sharing interface is consistent with the system style and supports Messages, Mail, AirDrop and other targets (30:17)
Speech Recognition Speech recognition
const recognition = new webkitSpeechRecognition();
recognition.continuous = true;
recognition.interimResults = false;
recognition.onresult = (event) => {
let transcript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
if (event.results[i].isFinal) {
transcript += event.results[i][0].transcript;
}
}
console.log(transcript);
};
recognition.onend = () => {
console.log('Recognition ended');
};
function startRecognition() {
recognition.start();
}
function stopRecognition() {
recognition.stop();
}
Key points:
- Uses the same speech recognition engine as Siri, supporting multiple languages (32:23)
- Still needed
webkitPrefix:new webkitSpeechRecognition()(33:25) continuousProperty controls whether recognition continues until manually stopped (33:36)- A privacy authorization prompt will pop up when using it for the first time (32:41)
- The results are sorted by probability, and the first one is the best recognition result (34:02)
Core Takeaways
- Replace WeakMap encapsulation with private class fields.
#fieldThe syntax is simpler and more intuitive than the WeakMap solution, and privacy is guaranteed at the language level. - Web Share file sharing makes Web App more like native.A few lines of code can call the system sharing panel to send files, and the user experience is consistent with the native app.
- MediaRecorder + Audio Worklet is combined to make audio applications.Recording, real-time processing, and sharing are all connected through one link, and a complete audio editing tool can be implemented on the Web.
- Speech Recognition lowers the threshold for voice interaction.Using the Siri engine to convert speech to text does not require training your own model or calling a third-party API.
- Module Workers move heavy calculations to the background.With ES Module’s dependency management and dynamic import, complex calculations no longer block the main thread.
Related Sessions
- Explore WKWebView additions — WKWebView latest updates and best practices
- Accelerate networking with HTTP/3 and QUIC — Web network acceleration
- Meet Safari Web Extensions — Getting started with Safari extension development
Comments
GitHub Issues · utterances