Parcel: 🙋 Regular Browser Reload on File Save

Created on 15 Dec 2017  ·  43Comments  ·  Source: parcel-bundler/parcel

🙋

Hi there! I'd like to start exploring Parcel as a workflow for creative coding (WebGL, Canvas2D, etc). I'm trying to migrate some programs I've written for non-HMR workflow to this new HMR workflow, and I'm hitting huge performance issues since the code (which is doing things like creating a WebGL/Canvas context) was not designed to be re-run multiple times.

🎛 Configuration

Take a simple program like this:

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

document.body.appendChild(canvas);

🤔 Expected Behavior

I'd like to develop with the same way that a JS file runs/loads in the browser (i.e. once, not many times). If possible, I'd like a way to replace JavaScript HMR with a simple window.location.reload() functionality. However, other features (like CSS) should still use HMR / inject without hard reload.

😯 Current Behavior

Currently the above code, when saved several times, will create several canvas elements in the body.

💁 Possible Solution

A way of turning on/off regular hot reload. I am assuming this may already exist, but I couldn't find it, so perhaps it's more an issue of documentation?

Feature HMR

Most helpful comment

I like the idea of a --reload flag, I much prefer it over HMR which I always opt out of.

All 43 comments

Give --no-hmr a try when you call parcel. It sounds like it may not be exactly what you're looking for, but it could be an improvement.

Thanks! But that just turns off reloading altogether... 😆

For now, I have a branch with the option --reload which will trigger a hard-reload on any non-CSS assets.

https://github.com/mattdesl/parcel/tree/feature/add-js-reload

Honestly I don’t think this has any practical use cases outside of the one you described.

Is there anywhere else where somebody would actually want this option? If so, feel free to submit a PR with your add-js-reload branch.

Honestly I don’t think this has any practical use cases outside of the one you described.

You mean, creating a new HTML element and adding it to the body? It's really one of the most basic things you can do with JS...

I am genuinely curious how I could change my application to better support HMR.

Here is another example: creating a loop with requestAnimationFrame:

window.requestAnimationFrame(render);
console.log('Starting loop...');

function render (time) {
  window.requestAnimationFrame(render);
  console.log('Rendering', time);
}

If you save the file twice you will end up with two requestAnimationFrame loops running simultaneously.

Simple, just move the animation logic to a separate module, that way you get hot module replacement :)

I didn’t actually try this for myself, but something like this should work:

index.js

import draw from './draw'

window.requestAnimationFrame(render);
console.log('Starting loop...');

function render (time) {
  window.requestAnimationFrame(render);
  draw(time)
}

draw.js

export default function draw(time){
    console.log('Rendering', time);
    //...
}

@DeMoorJasper The following doesn't work – nothing changes when saving/editing the draw.js file:

import draw from './draw';

document.addEventListener("DOMContentLoaded", function(event) {
  window.requestAnimationFrame(render);
  console.log('Starting loop...');

  function render (time) {
    window.requestAnimationFrame(render);
    draw(time)
    console.log('Rendering', time);
  }
});

@davidnagli That doesn't work either, when I save the draw.js module it triggers a hot reload in my index.js and the loop is re-started (causing duplicate rAFs).

That's why i removed it, i realised it would completely disable HMR @mattdesl

@mattdesl I was able to reproduce your issue, currently working on figuring it out.

For the simple use case, you can do a bit of DOM management to replace instead of append on every change:

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

document.body.replaceChild(canvas, document.body.lastElementChild);

Saving this will result in the canvas reloading.

For the animation frame scenario, you can manage the request id:

if (window.currentAnimationFrameId){
    window.cancelAnimationFrame(currentAnimationFrameId)
}

window.currentAnimationFrameId = window.requestAnimationFrame(render);
console.log('Starting loop...');

function render (time) {
  window.currentAnimationFrameId = window.requestAnimationFrame(render);
  console.log('Rendering', time);
}

This isn't "automatic", but with a bit of boilerplate it allows the code to be HMR-compatible.

I like the idea of a --reload flag, I much prefer it over HMR which I always opt out of.

@thejameskyle Interesting, why do you opt out?

Personally I would rather --hmr be opt-in than on by default, since it’s far more magical than just a simple page reload.

+1 on --reload

Hi everyone, if you really need regular reload but not HMR, you can try my fork https://github.com/TennyZhuang/parcel

just npm install git+https://[email protected]/TennyZhuang/parcel

Note that I have not make regular reload as an option now, so you can use it as same as the upstream, and there will always be a browser reload after every change.

I will try to make this as an option and make a merge request to the upstream if needed, @devongovett @brandon93s

@TennyZhuang did you see my fork? It includes a ‘reload’ option.

I can submit it as a PR. Personally I think it should be the default, but I guess my goals are a little different than those of the Parcel maintainers.

HMR is an important feature in parcel and faster than regular reload, I
think HMR as default behavior is reasonable, but i really need regular
reload in my three.js development.

It seems that your fork works, but I didn’t see it before I finish my fork.
On Sat, 30 Dec 2017 at 23:45 Matt DesLauriers notifications@github.com
wrote:

@TennyZhuang https://github.com/tennyzhuang did you see my fork? It
includes a ‘reload’ option.

I can submit it as a PR. Personally I think it should be the default, but
I guess my goals are a little different than those of the Parcel
maintainers.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/parcel-bundler/parcel/issues/289#issuecomment-354552657,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AIvK3lkRPsE7ID4NQetEjD7a6-wYiuwmks5tFlqPgaJpZM4RDoCb
.

@mattdesl feel free to submit a PR with it, would probably be much appreciated

HMR is an important feature in parcel and faster than regular reload, I think HMR as default behavior is reasonable, but i really need regular reload in my three.js development.

It's not really faster – in my own tests I've found parcel reloading to be pretty much comparable to budo, which uses hard page reloads. With small to medium bundle sizes, budo seems to reload faster, and with huge bundles (several MBs), parcel seems faster.

Currently HMR is not even working in Parcel as pointed out earlier in this thread. I don't really understand how anybody is using this in production right now. 🤷

Commenting here what I wrote on the PR so others can see. Seems to me like you could just hook into the existing HMR system to do a full page reload if you really wanted to.

if (module.hot) {
  module.hot.accept(function () {
    window.location.reload();
  });
}

However, I'm wondering why this is really needed? HMR is powerful because it allows you to maintain the state of your application across code changes, so you don't need to e.g. re-open a modal and click through 17 steps each time you make a change to the code.

@devongovett Yeah, that was the first thing I tried, although it feels like a hack. When I save a file, it triggers a hot module reload, so my code gets re-executed before the hot module replacement reloads the page.

This means any blocking code will slow down the reload cycle, and I end up with more memory usage during development. (Often the start of the application will create a WebGL context, generate geometries, and push features onto the GPU.)

However, I'm wondering why this is really needed? HMR is powerful because it allows you to maintain the state of your application across code changes, so you don't need to e.g. re-open a modal and click through 17 steps each time you make a change to the code.

I agree it can be very powerful, but only if the code is setup to work with it, and only in specific applications (e.g. React, Vue, etc). I've never been able to take advantage of application-wide HMR in a real WebGL project because of GL state, performance constraints, and things like that.

Anyways... as pointed out in this thread, hot module replacement is not working in Parcel, which is probably part of the reason some people are asking for a --reload option. Right now, any module change in your application will trigger a root-level reload, which means there is currently no clean way to avoid problems like window event listeners doubling up, simultaneous requestAnimationFrame loops, etc.

You could do something like this if you don't want the module to be re-executed:

if (module.hot) {
  module.hot.dispose(function () {
    window.location.reload();
  });
}

This will trigger the reload on module dispose rather than after the module has been re-executed.

You could also use that hook to store your state for later, and on accept restore it. HMR does take some work to get right, which is why things like react-hot-loader exist. Parcel is pretty much agnostic to that: it gives you hooks for when a module changes, it's up to you to decide what to do with that.

Right now, any module change in your application will trigger a root-level reload

That shouldn't be the case. The event starts at the module which changed, and bubbles up to the root. If you accept an update, the event stops bubbling up.

That shouldn't be the case. The event starts at the module which changed, and bubbles up to the root. If you accept an update, the event stops bubbling up.

Ok – this was never clear to me. It might be good to explain the system in the docs somewhere, rather than assume everybody is using React/Vue/etc.

So, how does one go about "setting up" HMR without copying the internals of react-hot-loader or similar modules? I really just want a basic app that reloads on file save (hence my PR), and I'd rather not add a deal of boilerplate to each file or have to carefully step around my code not to accidentally "Cmd + S" a certain file in case it duplicates window state.

The code here pretty much sums up the application I would like to build:
https://github.com/parcel-bundler/parcel/issues/289#issuecomment-352270143

Or is it basically React-or-nothing?

P.S.

To illustrate the issue with reload and thread blocking, try this example:

index.js

import * as THREE from 'three';

console.log('Creating...');
const sphere = new THREE.SphereGeometry(1, 512, 512);
console.log('Done');

if (module.hot) {
  module.hot.accept(() => {
    // or use this instead of dispose()
    // window.location.reload();
  });

  module.hot.dispose(() => {
    window.location.reload();
  });
}

Whether using dispose or accept, the JS blocks and waits for the geometry to be generated. So you end up waiting twice as long: once before the reload is triggered, and once after the page actually reloads. :\

@brandon93s Thanks, your solution works perfectly for me. Hence, I now have no need for the --reload option.

+1 on wanting an --reload option (or something). When using elm-lang in fullscreen mode, an entirely new instance of the elm app is appended to the DOM on any .elm files saves.

Using

if (module.hot) {
  module.hot.dispose(() => {
    window.location.reload();
  });
}

works, but there still is an second where the new content flashes before the page reload occurs. Plus still having .css module replaced would be ideal

What if we automatically called window.location.reload() for you if the HMR event bubbled all the way to the top without any module calling module.hot.accept()? Then, by default, you'd get window reloading, but if you added HMR code to handle events yourself, it would not reload. No option needed! Thoughts?

@devongovett This makes sense to me, I think that's what webpack dev server does in hot mode.

What about with asset types that implement hot module reloading themselves? Like it seems reasonable to make all CSS files support hot module reloading by default, but if you wanted to disable that behaviour for some reason, how could you?

I found a hacky solution to reload immediately:

// put this at the top of your code
if (module.hot) {
    module.hot.dispose(() => {
        window.location.reload();
        throw 'whatever'
    })
}

Or...

if (module.hot) {
    module.hot.dispose(() => {
        window.location.reload();
    })
} else { do_something(); }

I would like to have this functionality as well.

What if we automatically called window.location.reload() for you if the HMR event bubbled all the way to the top without any module calling module.hot.accept()? Then, by default, you'd get window reloading, but if you added HMR code to handle events yourself, it would not reload. No option needed! Thoughts?

Would this get implemented?

I would love to have a --reload feature. Maybe I'm a bad developer and can't get my app to work perfectly with HMR, but never ever did a bundler send my fine working app in an infinite loop and crashed my browser tab when I save a file before. This is kinda frustrating.

For anyone who is looking for a zero-config tool for canvas/WebGL prototyping, I ended up building my own tool, canvas-sketch, that approaches (hot) reloading differently, in a way that is more suitable for requestAnimationFrame and so on.

I'm running Parcel in a Django CMS setup. The HMR is fantastic for all my JS and CSS changes, however ParcelJS doesn't know anything about my Django HTML templates at this stage, nor is it possible or practical for Parcel to be used to bundle them in any way.

As such I don't get a reload when I update my templates. It would be great if I could just give Parcel the path to my templates directory and tell it to perform a hard reload whenever a change is detected there, but keep HMR support for the other stuff.

I know I should just implement browsersync for this sort of thing but it would seem a shame to add that as Parcel already handles almost everything I need/want for so little effort.

Is it/could it be possible to initiate a hard reload over the websocket from a custom plugin?

Being able to execute custom code on the client side could be pretty powerful (I have no idea if this already exists though)

Bump for the --reload feature. The js' hmr behavior is too brittle to even be the default reloading option, let alone the only one.

It can potentially lead to myriads of nasty bugs. Especially, in the hands of less-experienced developers. I'm not sure whether the hmr is worth such a price.

Thanks for the amazing work, btw.

I second this; I often have to manually restart the dev server when developing since it just stops working. That's on top of it getting stuck in an endless loop as per #1317.

+1 for --reload option

Guys, let's not get this to a 3130 level. @paulosborne, you would +1 it? Just hit the thumbs up button on the first post.

Thanks.

I am using a workaround.

Because I'm getting an error when parcel is updating: 'parcelRequire' of undefined

// page reload on parcel hmr
window.onerror = function(e) {
    if(e == `Uncaught TypeError: Cannot read property 'parcelRequire' of undefined`) {
        console.log('yess')
        location.reload();
    }
};

So, should both of the suggested solutions be implemented?

  • --reload flag for cases where HMR doesn't work at all/causes an infinite loop, an explicit opt-out
  • Automatic reload when hmr accept bubbled up and wasn't accepted (would be somewhat similar to webpack's default, seems a better default anyway): https://github.com/parcel-bundler/parcel/issues/289#issuecomment-379529297

If you want to reload after file changes, you can run Parcel in watch mode, which compiles changes to another directory (default ./dist/) but does not run a server.

parcel watch index.html --no-hmr

Then run a live-server, which listens to file changes and reloads (assuming your output is to the ./dist/ directory)

live-server ./dist/

Here's what my npm start script looks like:

parcel watch ./src/index.html --no-hmr & live-server --port=8000 ./dist/

In #2676 which just landed in master, the default was changed to reload the page unless you call module.hot.accept() in your app. We found that HMR seems to fail more often than not, so made it an opt-in feature.

Was this page helpful?
0 / 5 - 0 ratings