MessagePorts in Electron

    Here is a very brief example of what a MessagePort is and how it works:

    renderer.js (Renderer Process)

    main.js (Main Process)

    1. // In the main process, we receive the port.
    2. ipcMain.on('port', (event) => {
    3. // When we receive a MessagePort in the main process, it becomes a
    4. // MessagePortMain.
    5. const port = event.ports[0]
    6. // MessagePortMain uses the Node.js-style events API, rather than the
    7. // web-style events API. So .on('message', ...) instead of .onmessage = ...
    8. port.on('message', (event) => {
    9. // data is { answer: 42 }
    10. const data = event.data
    11. })
    12. // MessagePortMain queues messages until the .start() method has been called.
    13. port.start()
    14. })

    The Channel Messaging API documentation is a great way to learn more about how MessagePorts work.

    In the renderer, the MessagePort class behaves exactly as it does on the web. The main process is not a web page, though—it has no Blink integration—and so it does not have the MessagePort or MessageChannel classes. In order to handle and interact with MessagePorts in the main process, Electron adds two new classes: MessagePortMain and . These behave similarly to the analogous classes in the renderer.

    MessagePort objects can be created in either the renderer or the main process, and passed back and forth using the ipcRenderer.postMessage and methods. Note that the usual IPC methods like send and invoke cannot be used to transfer MessagePorts, only the postMessage methods can transfer MessagePorts.

    By passing MessagePorts via the main process, you can connect two pages that might not otherwise be able to communicate (e.g. due to same-origin restrictions).

    Electron adds one feature to MessagePort that isn’t present on the web, in order to make MessagePorts more useful. That is the close event, which is emitted when the other end of the channel is closed. Ports can also be implicitly closed by being garbage-collected.

    In this example, the main process sets up a MessageChannel, then sends each port to a different renderer. This allows renderers to send messages to each other without needing to use the main process as an in-between.

    main.js (Main Process)

    1. const { BrowserWindow, app, MessageChannelMain } = require('electron')
    2. app.whenReady().then(async () => {
    3. // create the windows.
    4. const mainWindow = new BrowserWindow({
    5. show: false,
    6. webPreferences: {
    7. contextIsolation: false,
    8. preload: 'preloadMain.js'
    9. }
    10. })
    11. const secondaryWindow = new BrowserWindow({
    12. show: false,
    13. webPreferences: {
    14. contextIsolation: false,
    15. preload: 'preloadSecondary.js'
    16. }
    17. })
    18. // set up the channel.
    19. const { port1, port2 } = new MessageChannelMain()
    20. // once the webContents are ready, send a port to each webContents with postMessage.
    21. mainWindow.once('ready-to-show', () => {
    22. mainWindow.webContents.postMessage('port', null, [port1])
    23. })
    24. secondaryWindow.once('ready-to-show', () => {
    25. secondaryWindow.webContents.postMessage('port', null, [port2])
    26. })
    27. })

    Then, in your preload scripts you receive the port through IPC and set up the listeners.

    preloadMain.js and preloadSecondary.js (Preload scripts)

    1. const { ipcRenderer } = require('electron')
    2. ipcRenderer.on('port', e => {
    3. // port received, make it globally available.
    4. window.electronMessagePort.onmessage = messageEvent => {
    5. // handle message
    6. }
    7. })

    In this example messagePort is bound to the window object directly. It is better to use contextIsolation and set up specific contextBridge calls for each of your expected messages, but for the simplicity of this example we don’t. You can find an example of context isolation further down this page at

    That means window.electronMessagePort is globally available and you can call postMessage on it from anywhere in your app to send a message to the other renderer.

    renderer.js (Renderer Process)

    In this example, your app has a worker process implemented as a hidden window. You want the app page to be able to communicate directly with the worker process, without the performance overhead of relaying via the main process.

    1. app.whenReady().then(async () => {
    2. // The worker process is a hidden BrowserWindow, so that it will have access
    3. // to a full Blink context (including e.g. <canvas>, audio, fetch(), etc.)
    4. const worker = new BrowserWindow({
    5. show: false,
    6. webPreferences: { nodeIntegration: true }
    7. })
    8. await worker.loadFile('worker.html')
    9. // The main window will send work to the worker process and receive results
    10. // over a MessagePort.
    11. const mainWindow = new BrowserWindow({
    12. webPreferences: { nodeIntegration: true }
    13. })
    14. mainWindow.loadFile('app.html')
    15. // We can't use ipcMain.handle() here, because the reply needs to transfer a
    16. // MessagePort.
    17. // Listen for message sent from the top-level frame
    18. mainWindow.webContents.mainFrame.on('request-worker-channel', (event) => {
    19. // Create a new channel ...
    20. const { port1, port2 } = new MessageChannelMain()
    21. // ... send one end to the worker ...
    22. worker.webContents.postMessage('new-client', null, [port1])
    23. // ... and the other end to the main window.
    24. event.senderFrame.postMessage('provide-worker-channel', null, [port2])
    25. // Now the main window and the worker can communicate with each other
    26. // without going through the main process!
    27. })
    28. })

    worker.html

    1. <script>
    2. const { ipcRenderer } = require('electron')
    3. const doWork = (input) => {
    4. // Something cpu-intensive.
    5. return input * 2
    6. }
    7. // We might get multiple clients, for instance if there are multiple windows,
    8. // or if the main window reloads.
    9. ipcRenderer.on('new-client', (event) => {
    10. const [ port ] = event.ports
    11. port.onmessage = (event) => {
    12. // The event data can be any serializable object (and the event could even
    13. // carry other MessagePorts with it!)
    14. const result = doWork(event.data)
    15. port.postMessage(result)
    16. }
    17. })
    18. </script>

    app.html

    1. <script>
    2. const { ipcRenderer } = require('electron')
    3. // We request that the main process sends us a channel we can use to
    4. // communicate with the worker.
    5. ipcRenderer.send('request-worker-channel')
    6. ipcRenderer.once('provide-worker-channel', (event) => {
    7. // Once we receive the reply, we can take the port...
    8. const [ port ] = event.ports
    9. // ... register a handler to receive results ...
    10. console.log('received result:', event.data)
    11. }
    12. port.postMessage(21)
    13. })
    14. </script>

    Electron’s built-in IPC methods only support two modes: fire-and-forget (e.g. send), or request-response (e.g. invoke). Using MessageChannels, you can implement a “response stream”, where a single request responds with a stream of data.

    renderer.js (Renderer Process)

    main.js (Main Process)

    1. ipcMain.on('give-me-a-stream', (event, msg) => {
    2. // The renderer has sent us a MessagePort that it wants us to send our
    3. // response over.
    4. const [replyPort] = event.ports
    5. // Here we send the messages synchronously, but we could just as easily store
    6. // the port somewhere and send messages asynchronously.
    7. for (let i = 0; i < msg.count; i++) {
    8. replyPort.postMessage(msg.element)
    9. }
    10. // We close the port when we're done to indicate to the other end that we
    11. // won't be sending any more messages. This isn't strictly necessary--if we
    12. // didn't explicitly close the port, it would eventually be garbage
    13. // collected, which would also trigger the 'close' event in the renderer.
    14. replyPort.close()
    15. })

    When is enabled, IPC messages from the main process to the renderer are delivered to the isolated world, rather than to the main world. Sometimes you want to deliver messages to the main world directly, without having to step through the isolated world.

    main.js (Main Process)

    1. const { BrowserWindow, app, MessageChannelMain } = require('electron')
    2. const path = require('path')
    3. app.whenReady().then(async () => {
    4. // Create a BrowserWindow with contextIsolation enabled.
    5. const bw = new BrowserWindow({
    6. webPreferences: {
    7. contextIsolation: true,
    8. preload: path.join(__dirname, 'preload.js')
    9. }
    10. })
    11. bw.loadURL('index.html')
    12. // We'll be sending one end of this channel to the main world of the
    13. // context-isolated page.
    14. const { port1, port2 } = new MessageChannelMain()
    15. // It's OK to send a message on the channel before the other end has
    16. // registered a listener. Messages will be queued until a listener is
    17. // registered.
    18. port2.postMessage({ test: 21 })
    19. // We can also receive messages from the main world of the renderer.
    20. port2.on('message', (event) => {
    21. console.log('from renderer main world:', event.data)
    22. })
    23. port2.start()
    24. // The preload script will receive this IPC message and transfer the port
    25. // over to the main world.
    26. bw.webContents.postMessage('main-world-port', null, [port1])
    27. })

    preload.js (Preload Script)

    1. const { ipcRenderer } = require('electron')
    2. // We need to wait until the main world is ready to receive the message before
    3. // sending the port. We create this promise in the preload so it's guaranteed
    4. // to register the onload listener before the load event is fired.
    5. const windowLoaded = new Promise(resolve => {
    6. window.onload = resolve
    7. })
    8. ipcRenderer.on('main-world-port', async (event) => {
    9. await windowLoaded
    10. // We use regular window.postMessage to transfer the port from the isolated
    11. // world to the main world.