Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Version compatibility issues #90

Open
priyakanth024 opened this issue Aug 8, 2016 · 17 comments
Open

Version compatibility issues #90

priyakanth024 opened this issue Aug 8, 2016 · 17 comments

Comments

@priyakanth024
Copy link

Hello, using an older version of fastdom (0.8.6) throws an error with read and write methods when overridden by bowser or website's newer version of fastdom - they're replaced with measure and mutate. Is there a workaround you'd suggest to make the older version not break?

Here's an example: https://jsfiddle.net/10hn9hd6/
Browser used: Chrome 51.0.2704.103 (64-bit)

@wilsonpage
Copy link
Owner

I'm going to need some more info here, I don't quite follow. Where is the error/breakage in the Fiddle you linked to?

@priyakanth024
Copy link
Author

Thanks for the response.

So this is the right rendering of the elements on the page: https://jsfiddle.net/hyk3k554/ - this uses fastdom 0.8.6 as an external resource

But here is where it breaks when the latest fastdom (1.0.3) is used: https://jsfiddle.net/10hn9hd6/

The whole reason for this is widgets.js which Twitter uses to render the widgets use 0.8.6. Since index.js of 0.8.6 overrides var fastdom with window.fastdom when its available, it loads the latest version and throws widgets.js:8 Uncaught (in promise) TypeError: u.write is not a function

@wilsonpage
Copy link
Owner

Ah OK I see the issue now.

Possible quick fix:

  1. Load fastdom.js (v1)

  2. Add some backward compatibility support

    window.fastdom.read = window.fastdom.measure;
    window.fastdom.write = window.fastdom.mutate;
  3. Load widget.js


If this hack works out, we could look at writing something similar into the core.

@indianburger
Copy link

Hello,

I work with Priya and I can add more context. We are from Twitter and the maintainers of widgets.js, not the host page. We are third party javascript on a page, we have no control on what version of fastdom is run in the host page. We bundle fastdom in widgets.js (with npm and webpack as a require('fastdom')). When we do a require('fastdom') in our code, we always want the version that we picked to bundle, never what's on window.

I do not know the history of why you check for window.fastdom before injecting it so apologies if I'm oversimplifying: perhaps you can check if it's a common-js environment, and drop the check?

@indianburger
Copy link

Oh, and I'm happy to submit a PR if that's something you are willing to accept.

@wilsonpage
Copy link
Owner

Thanks for your help on this guys, this is a really interesting bug :)

We cannot simply drop the check and allow widgets.js to have its own fastdom instance, as this essentially breaks fastdom. If two instances of fastdom exist in the same document, they will schedule two different batches of task execution. This means that 'measure' and 'mutate' tasks can collide, forfeiting the benefits of using fastdom in the first place.

I think there are two cases we need to support:

A. New version of fastdom loaded, then old version of fastdom loaded via widgets.js.
B. Old version of fastdom loaded (via widgets.js), then new version.

The first fastdom loaded always wins.

I think the best solution is:

  • I add backward compatibility API to fastdom@latest that aliases .measure() as .read() and .mutate() as .write().
  • You guys upgrade widgets.js to fastdom@latest and make sure you call the .read() and .write() APIs.

This means that if widgets.js will work with old or new fastdom and also won't impact user scripts after it's loaded.

Does this sound sensible?

@indianburger
Copy link

There are a few downsides with this:

  1. In commonjs/amd when I bundle a version of fastdom, I expect to run that version of it which I have tested and built.
  2. commonjs/amd modules do not leak globals in window which fastdom currently does, but it didn't do that before. As a 3rd party script, we can't leak globals.
  3. Somewhere before 1.0.3 you didn't install fastdom in window (you didn't do it in 0.8.6). Which means if someone used commonjs bundling before, there is no way another fastdom script running in the same page can detect that it exists (because commonjs [email protected] didn't modify window.fastdom)
  4. What if there's another library called fastdom or a variable in window that is already called fastdom? This the sort of thing module systems take care.

I agree running 2 instances of fastdom is suboptimal but it's not the worst either. It is better than not using fastdom at all, and people could always implement their own version of layout batching which becomes less optimal than a single instance of fastdom. 2 instances is still better than none because each of the instance is batching reads and writes and collisions are an edge case right? Correct me if my assumptions are wrong here.

Considering that, would you let 2 instances of fastdom run in a commonjs/amd case?

@wilsonpage
Copy link
Owner

We did use to do something similar in v0.8.6, but you're right, if using a module system you wouldn't pollute the global namespace. But that isn't ideal as is could result in you having multiple instances of fastdom in a single app.

I believe having more than one instance of fastdom in an app really is worse than having none. The reason DOM is slow is because it's shared by all scripts on a page and any one of them can read or write to the DOM at any time and collisions are inevitable. Fastdom's job is to act as a single gatekeeper to the DOM for all these scripts, ensuring that scheduled tasks are 'coalesced' in a way that works with the DOM's life-cycle.

Let's say you had two UI components, each with their own version of fastdom. When they layout they need each need to schedule a 'measure' and then 'mutate' task.

Component A:

  • measure
  • mutate

Component B:

  • measure
  • mutate

Each component's fastdom will schedule it's own requestAnimationFrame in which it flushes the two tasks. Component A measures and mutates, this makes the DOM dirty (as the async style,layout,paint pipeline hasn't updated yet), then Component B measures (which forces the dirty DOM to perform a forced-sync-layout) and mutates.

The more fastdom instances you have, the greater the risk of introducing forced-sync-layouts, which fastdom's main task is to avoid.

Does that make any more sense? Sorry if I'm not too good at explaining.

@indianburger
Copy link

First, thanks for being so responsive.

I think I understand what you say and I'll try to make my case, hopefully clearer.

I agree that having multiple fastdom instances is bad. However, in a large website built with libraries, each library wants to do the right thing and batch reads and writes. And these libraries do not all not use fastdom, which is the reality, and that means inevitably there will be layouts thrashing. Unless there's a browser spec to do this natively, there will always be layout thrashing. Also we are talking about an edge case where fastdom is included multiple times and in this edge case I argue that multiple fastdom instances is better than the downsides. The downsides are that you are breaking versioning and always leaking a global. I could be a website owner running latest fastdom but I might be running an old version of fastdom without my knowledge because some library used it. That's my argument for allowing multiple fastdom instances when used in a module system, but there's also a workaround for us without running multiple instances.

The more narrower fix for my specific case is to not leak a global, just for commonjs/amd. We are ok to use the fastdom instance that the host page uses even if its older(through your aliasing suggestion), but our main concern is we don't want to leak a global. widgets.js is running on a lot of websites and we try to never pollute their global window.

In the end, I can see that it's not by the philosophy of your library to not be optimal about layout especially when that's the point of your library. Perhaps a fork where we don't pollute the namespace is the solution for the widgets.js case. But forks are bad, especially when you have such a well maintained library so hopefully there's a middleground. Perhaps you have other suggestions which avoid setting a global from widgets.js?

@steffenweber
Copy link

I originally reported this issue a few days ago on the Twitter Developer Forums: JavaScript error in Widgets-JS on websites using FastDOM 1.0

@wilsonpage said one possible solution could be:

I add backward compatibility API to fastdom@latest that aliases .measure() as .read() and .mutate() as .write().

That's what I did a few months ago. But this workaround broke 1 week ago when Twitter started using the method fastdom.defer, too. This method seems to have been removed in FastDOM 1.0. I've since added another hack that does not really do what fastdom.defer is supposed to do but solves this specific issue for us:

win.fastdom.read = win.fastdom.measure;
win.fastdom.write = win.fastdom.mutate;
win.fastdom.defer = function(frame, fn, ctx) {
    fastdom.measure(fn, ctx);
}

The point is: fastdom.defer would have to be emulated, too. Or Twitter has to stop using this method and commit to only use fastdom.read and fastdom.write.

@wilsonpage
Copy link
Owner

wilsonpage commented Aug 12, 2016

OK, sounds like this is getting messy. Here are the options I can think of:

A. We encapsulate the fastdom instance and allow multiple versions to run on a page:

  • Good: Because libraries and apps can trust they are getting the actual version/API they installed
  • Bad: Multiple versions of fastdom can collide and thrash the DOM.

B. Libraries (like Twitter's widgets.js) could establish a convention of exposing their fastdom instance (eg. twitter.fastdom) to consumers so the performance conscious have the choice of using a pre-existing instance.

  • Good: Gives consumers a choice
  • Bad: Consumers don't know which version of fastdom their getting.
  • Bad: Requires evangelism and conventions
// consumer code
var fastdom = twitter.fastdom || require('fastdom');

C. Libraries which play with the DOM (like widgets.js) could optionally allow users to pass in a fastdom-like API so that app owners can be sure that third-party libraries measure and mutation tasks will coalesce with their own tasks.

twitter.setDomManager({
  measure: fastdom.measure.bind(fastdom),
  mutate: fastdom.mutate.bind(fastdom),
});

D. Fastdom internally checks for pre-existing versions on a more obfuscated window.__fastdom__ global. Upon finding an older version it shims it with newer .measure and .mutate methods. Upon finding newer version it shims it with older APIs (.read(), .write(), .defer()). So although technically the user it getting a different version of fastdom back than they asked for, the public interface will conform to what they are expecting.

  • Good: Fastdom can remain a singleton and DOM tasks run smooth all the time
  • Good: Requires no additional work from consumers
  • Bad: Mutating objects like this is quite hacky and could be error prone.

Let's discuss these options. If anyone has any additional idea, please put them forward.

@Isinlor
Copy link

Isinlor commented Aug 12, 2016

Sorry for jumping in the discussion out of blue, but isn't option C generally accepted as the best practice known as dependency inversion principle? As far as I can see it solves the issue fully without creating any drawbacks form technical point of view.

It allows libraries to depend on abstract, self-defined and therefore stable interface. It solves the issue with many instances of fastdom for end user. It even allows to replace fastdom with some other implementation if fastdom for some reason won't be fitting library end user preferences eg. if browser will start to do it natively.

IMHO option C should be put in README as a best practice for library creators :) .

@domenic
Copy link

domenic commented Aug 12, 2016

@wilsonpage asked me for opinions on the best solution here. This is a tough problem. Most libraries don't have this kind of global coordination problem; the worst thing that happens when multiple instances are used on a page is some code bloat. That means most libraries work well with module system-based isolation. But I can see where fastdom is different.

I see a two main paths:

  • Do nothing, or do something minimal like A
    • If you are using multiple copies of the global version of fastdom, you are hosed, just like if you use multiple copies of the global versions of jQuery, or Backbone, or whatever. Sorry.
    • Allow multiple module-based (or otherwise somehow isolated) versions in the same page. Pages will take a perf hit compared to the optimal single-read/single-write batches, but the batching should still help, even if there are multiple of each.
  • Try to solve the coordination problem transparently for all consumers.
    • Because fastdom needs a unique global coordination channel, it must break out of any module system, in an unusual way. window.__fastdom__ is one way to do this. Or try to hide it somewhere else, e.g. using Symbol.for or similar in modern browsers.
    • This could be combined with a version that mutates the globals to uniformize the interface (similar to D, I believe). Or it could be combined with a move to encapsulate fastdom instances, possibly with a module system (similar to A)---but the encapsulated instances would all coordinate through a shared window.__fastdom__ API.
    • This shared API must never change, even if the main fastdom API goes through breaking changes.

Personally I don't think B or C buys much; they require people to use them correctly, so they are only marginal gains over "do nothing", and not as user-friendly as "try to solve the coordination problem transparently".

@wilsonpage
Copy link
Owner

@domenic thanks for your perspective, mega appreciated!
@indianburger how would you feel about a variation on option D.?

@indianburger
Copy link

I'm with Domenic, either A or D sounds ok. I think there's also another option E[1] we could explore but I'm not sure how feasible it is.

I would go with A because:

  • with D we are only patching conflicts between future versions beyond fastdom 1.0.3 like e.g. 1.0.4 and 1.0.5 existing on the same page. I find that versions conflicts are already super rare for fastdom and option D only fixes for future version conflicts so it's extra rare.
  • A gives you an implementation you can rely on. If there's a bug fix in fastdom, I can be sure I'll be running it.
  • A is lesser work for fastdom.

I got one suggestion if you go with D. You had a major version upgrade which actually made the api incompatible, and in a future 2.x there could be another breaking change. I would suggest a namespace that accounts for the major version so we know that the api is always backwards compatible. e.g. window.__fastdom_1.x. That way you don't have to worry about aliasing and adding back defer which defeats the purpose of your major version upgrade.

[1]: Option E: This is based on my rudimentary understanding of Fastdom implementation. Could we make the fastdom module private, but the intenral read and write queues globally shared in window. If the queue data structure is stable across versions, then we can have bug fixes and version upgrades be more meaningful because you'll be running the version you expect but use the global queue for storage. (I'm being super naive about race conflicts, so this option might be infeasible)

@wilsonpage
Copy link
Owner

[1]: Option E: This is based on my rudimentary understanding of Fastdom implementation. Could we make the fastdom module private, but the intenral read and write queues globally shared in window. If the queue data structure is stable across versions, then we can have bug fixes and version upgrades be more meaningful because you'll be running the version you expect but use the global queue for storage. (I'm being super naive about race conflicts, so this option might be infeasible)

I think this is a good solution and is basically what @domenic suggested above in his last few bullets. The downside is that it won't be a backwards compatible change, so consumers on v0.8 would still have issues but > v1.0.3 would be able to use this shared global piece. Perhaps this would warrant a v2.0 release and encourage everyone to upgrade from lower versions.

@indianburger would you be happy with this 'shared global singleton piece' being stored/leaked onto a window.__fastdom__ property?

This shared piece would effectively be a global task queue. It would have two lists 'measure' and 'mutate' and would flush on requestAnimationFrame. Fastdom would require() this global singleton module and wrap it's documented interface around it.

My concern with this approach is that fastdom is actually already a tiny library. I would imagine that most of it's code would end up being moved to this GlobalSingletonTaskQueue and we'd just be moving the problem to different place.

It may be wishful thinking, but I'm not planning on changing the fastdom API. IMO the library is mature and 'finished' (touchwood). In v1 I added the .extend() feature which should allow other to bolt-on higher level APIs to the fastdom global-singleton and still share the same underlying task queue.

@indianburger
Copy link

👍 window.fastdom solution sounds good to me.

On Tue, Aug 16, 2016, 6:13 AM Wilson Page [email protected] wrote:

[1]: Option E: This is based on my rudimentary understanding of Fastdom
implementation. Could we make the fastdom module private, but the intenral
read and write queues globally shared in window. If the queue data
structure is stable across versions, then we can have bug fixes and version
upgrades be more meaningful because you'll be running the version you
expect but use the global queue for storage. (I'm being super naive about
race conflicts, so this option might be infeasible)

I think this is a good solution and is basically what @domenic
https://github.com/domenic suggested above in his last few bullets. The
downside is that it won't be a backwards compatible change, so consumers on
v0.8 would still have issues but > v1.0.3 would be able to use this
shared global piece. Perhaps this would warrant a v2.0 release and
encourage everyone to upgrade from lower versions.

@indianburger https://github.com/indianburger would you be happy with
this 'shared global singleton piece' being stored/leaked onto a
window.fastdom property?

This shared piece would effectively be a global task queue. It would have
two lists 'measure' and 'mutate' and would flush on requestAnimationFrame.
Fastdom would require() this global singleton module and wrap it's
documented interface around it.

My concern with this approach is that fastdom is actually already a tiny
library. I would imagine that most of it's code would end up being moved to
this GlobalSingletonTaskQueue and we'd just be moving the problem to
different place.

It may be wishful thinking, but I'm not planning on changing the fastdom
API. IMO the library is mature and 'finished' (touchwood). In v1 I added
the .extend() feature which should allow other to bolt-on higher level
APIs to the fastdom global-singleton and still share the same underlying
task queue.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#90 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAbaQkKVa5q5fHNrMHruaPiQdQFd2Jjnks5qgbdegaJpZM4JfUQK
.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants