-
Notifications
You must be signed in to change notification settings - Fork 931
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
Ideas for lit-html 2.0 #1182
Comments
Would you consider shortened urls for error messages in prod?
|
@bennypowers that's the idea for the errors that remain in prod. Some warnings might require string or DOM introspection that we just don't want to do in the prod build. Things like trying to verify that the template is well-formed. That may be better served still by static analysis with |
oh ok cool :D |
Lack of spread expressions are a big deal for me for design system projects. It's really valuable when designing component to wrap arbitrary components which can lift the props they need. |
I saw another member of the Google make a comment on a similarly shaped issue in a repo for a similarly small renderer wherein he implied that said renderer's future version should target being "2kb 2kb 2kb". Would it even be feasible to target a similar goal here? Keeping the library under 3kb would certainly seem easier while targetting getting it down to that level. |
I will try to give feedback on some of the things that came to mind.
I always thought this was "rocket science" and there has never been a need to dig deeper, for me. I wouldn't even know where to find further info to unleash the potential maybe buried there. So, I am team reducing the surface. :)
My gut reaction was that I find these a good reference point to see how something like this could be implemented. We do not use them, but we used them as kinda reference points to see how we could get where we wanted to get. I have a couple feelings about these:
Am I reading this correctly? You would remove support for attribute binding if the bound value is an array? I can see this being useful, while in those cases I prefer property bindings. Not sure I would support this change if it can be avoided.
I agree. I feel the library should not carry fixes for browsers that are not the release target of the ES version. For IE 11 we need to transpile lit-html anyway, so I think making polyfills an addon is fine. On a different note I think the ease of use of the web component polyfills in general could be improved.
So, this is a build step? Would this be opt in or the only way to use lit-html going forward? I think one of the beauties of lit-html is that you can just drop it in and it just works. I would also argue it makes it easier to understand. If I have to understand a template, a library, a compiler and possibly a build chain just to debug a bug it would increase complexity a lot I think. That does not mean I am against an optional webpack plugin or whatever though. |
@Westbrook I think we could make a very minimal lit-html in under 2KB, but we can deliver a lot of functionality in another 1KB or so. It's all tradeoffs. For apps there's diminishing returns on removing 1KB out of 100s or 1000s. For individual components it might really matter. We might be able to make a version of lit-html that only works if you're pre-compiled the templates (and is automatically swapped in), and individual components could use that in a build process to produce the smallest possible output. |
If you'd like another data point, we were just able to finally EOL IE11 in our application -- it took a 1.5 year company-wide effort. So we'd definitely support this! We do however still support legacy-Edge, but are monitoring Edgium uptake closely and hope to fast-track its EOL in the next 6-12 months. |
Maybe a Also +1 on the css - I remember being rather confused that I couldn't import { css } from 'lit-html'. |
This would only be for attributes, where right now we join the array items with a space.
Optional. It would perform the template preparation steps (adding markers, recording part metadata) at build-time. |
Thanks for your great work and efforts to keep lit-html one of the most well documented, well maintained, well supported etc. and stabile libraries that I have come across! This issue "Ideas for .." is just yet another example of that! Thanks!!! I am not using TypeScript - and I believe all you can do to improve the build process especially minifying html, css (less important as it is written more flat already than html nested syntax...) for a pure js ES6+ webpack or roll-up environment will be highly appreciated. Although there are examples it is not straight forward to get started and get a good minified version (which I believe are also more performant) of the render templates. I am using lit-virtualizer - but ran into the problem that it can't do more than rather simple fixed width listings. As soon as you go for MDC-cards in a grid or a bigger old-fashioned TABLE (where column width per definition are adjusted to actual cell content) - it is a no-go with lit-virtualizer. I have made some attempts with lit-element + Observer and simply re-render new entries to the end of the TABLE as you scroll down - and that works. It is simple and straight forward - however as soon as you reach a few 100 records there is a need to remove hidden previous records to maintain scroll speed and keep performance / memory down. That is more tricky (scroll position needs to be handled etc.) and I haven't got the solution to that (...yet)... However from that work I believe the main speed improvement for lit-html could actually be to guard (optionally) what is actually in the visible part of DOM / viewport... not sure whether that is addressed elsewhere, here or? (or is that what the repeat directive is actually doing / supposed to be doing...??) Streaming contents into a render process is also an area that I believe could be a (potentially more than SSR... or in combination with) performance improvement that could benefit from prepared features around lit-element and lit-html. |
Okay, let me explain a bit more where I am coming from. My idea has always been that when we write custom elements we, as development team, should provide attributes for our elements, because it is the HTML standard way of doing this. Because quite frankly nobody knows how and with which technology our elements might be used in the future. We are using LitElement, but tomorrow somebody might want to use them with Angular or whatever. That was my motivation for always providing attributes. However, upon further inspection this might have been a bit shortsighted: Since HTML attributes are per spec strings, there seems to be not really a native way to provide array or object attributes anyway. Maybe one could argue that the HTML spec tries to solve arrays more through the template: <fieldset>
<input type="radio" value="One">
<input type="radio" value="Two">
</fieldset> So, for element authoring array attribute binding is probably not the way to go anyway. And in lit-html we can always use property bindings. Not entirely sure, about object attribute bindings, it would kinda be the same thing right?
Sounds like a good idea. I am generally excited for the direction |
Would this involve exposing a way at run-time to create instances of a single template programmatically? In our case, we're struggling to come up with a clean solution whereby we can take rows of data from the backend and render them with a single template on the frontend. Normally, this is easy but in our case which template we use depends on the data which came back (or should...). This sounds similar SSR i guess, in that SSR has to have some way of instantiating a template (re-using it) with data? Basically i think the ultimate solution for us is the "template instantiation" RFC ive seen floating around for the past few years. But that won't come any time soon i imagine. i.e. we want this: render() {
return someAlreadyCompiledTemplate.apply({some: "data"});
} There are ways to achieve this in a more hard-coded/strongly-typed fashion but this could still be useful. |
I've been using lit-html for some time now and here are my suggestions :
|
@grimly thanks for the suggestions.
You absolutely can. If you're having an problem with this can you open an issue? |
Yay to "Remove use of instanceof"...while it's certainly manageable, it's always a chore and if we get that library all the way down to 2KB 😉 , it won't matter much if there are multiple copies on the page. With the continued growth of tools like https://medium.com/storybookjs/storybook-controls-ce82af93e430 the need for an updated scanner and native support for spreading objects onto elements is growing as well. Do you see some alternatives to needing to mutate object properties to be pre-fixed with sigils in order to acheive event and property binding? I like the overall drive to reduce layers of unneeded complexity and see this as yet another that it would be great to see a path towards disolving. Is there anything that an updated scanner or approach to directives could do inorder to increase the compatibility between On the subject of Preact and in a way the subject of getting down to 2KB (or less?), there is a proposal that would bring a (virtual) DOM diffing approach to templates natively to the browser in a way that could be naively related to what the Template Instantiation proposal also attempts to acheive. While you mention stepping back a little from the Template Instantiation spec while it bakes a little further, do you see any benefit of doing explorations into how Looking forward to ongoing expirementation! |
Oops. That was an issue of mine and I didn't take the time to look further. I stroke this point. |
For the record, I don't think we'll get to 2K. Let's say I'll be happy with a 20% improvement :)
Can you clarify? I see the need to set properties based on the "arguments", in our case the component's public properties. But why would spread bindings in lit-html help? Seems like the controls system could just set properties on the components directly, independent of lit-html.
What I've been experimenting with is two things: 1) The ability to have attribute-positioned expressions. Directives placed there could then do whatever they want in terms of setting properties, attributes, etc. (module the restrictions we want to ensure SSR compatibility. TBD). 2) an
This is nice because it's statically analyzable, doesn't require imperative DOM calls, and should be SSR-friendly. It could be used directly by template authors or produced by directives, but doesn't allow for dynamic bindings. For the case of turning objects into attribute bindings, I'd want to make that SSR-compatible, and without requiring custom SSR implementations for the directives. I'm not sure how possible that is without some tough code size/complexity tradeoffs. We'll see. It could be that for a lot of use-cases we're either fine with the more static |
Thanks for the transparency in this thread, really appreciate it. Love all of it! Here's some of what we're doing with lit: TemplateProcessorHere are the lit-html internals that we're using currently to customize lit for our projects. Mainly this comes down to us using our own
DirectivesAlso, we use directives heavily. For example, we do virtualization with a directive that loads and unloads the content depending on an automatically spawned export const virtualize = directive( (actualTemplateResult: TemplateResult) => (nodePart: NodePart) => {
const node = nodePart.startNode.parentElement!;
const render = () => {
...
};
const unrender = () => {
...
};
requestAnimationFrame(() => {
const intersectionObserver = new IntersectionObserver(
x => {
if (x[0].isIntersecting) render();
else unrender();
},
{ options },
);
intersectionObserver.observe(node);
// register unmount listeners // EDIT: this is something we add to parts to be able to stop rxjs subscriptions in parts
(nodePart as OptionallyBindingNodePart).subscription.add(() => {
intersectionObserver.unobserve(nodePart.startNode.parentElement!);
intersectionObserver.disconnect();
});
});
},
); Another example where we use directives is a preloader that spawns all components and sets style so that you can seamlessly switch between templates without a render flicker. That could probably be done with a standard web component as well though. Really excited to see where lit is going. Glad to provide feedback and beta testing at any time. |
Thanks for the info @yorrd! Hopefully you'll be able to handle the removal of We'll publish preview builds relatively soon. They'll have some missing features and won't have full browser support at first, but hopefully you can see how your needs fit. |
Couple of things im sure you're already aware of but these are what we've been hoping for:
Is there anywhere we can read about what work you've been doing in regards to SSR too? it would be nice to have an explainer of how you're looking to make it work so we're prepared/can track it. |
Coupling this kind of feature with directives may be dangerous. What if a second commit from a directive does not spread a previously given attribute ? Remove it ? Let it as is ?
For the
Are you talking about lit elements ? I can't find the use case otherwise.
There are a lot of new branches. Have a look in the commit network As for new suggestions : - export const nothing = {};
- export const noChange = {};
+ export const nothing = undefined;
+ export const noChange = Symbol.noChange = Symbol.for('noChange'); |
as @43081j already mentioned, I like to vote +1 to know a little more on the SSR and how would |
@justinfagnani thanks for your take on this. Appreciate the insight. We'll definitely beta-test as early as possible :) As for rxjs parts: I'm not sure whether directives are the way to go there just because you end up writing a whole lot more |
+1 to clarifying the type of |
Are there any plans for implementing this #1143 in the new Directives API? This would be useful in lit-element when we need to make directives aware of the custom element they are used in. |
Just tried version 2.0 <div>
<input type="date" .valueAsDate=${null} />
<input type="date" .valueAsDate=${undefined} />
<input type="date" .valueAsDate=${new Date()} />
</div> Use this and set to version 2.0.0-pre3 |
Question about my last comment. Should I create a seperate issue about this ? Really want 2.0 to be better and faster :-) |
For issues on the prerelease, please file an issue with the prefix and/or label |
The 2.0 release is imminent... thanks for the ideas! |
lit-html 1.x has been out for about a year and a half now. It's seen good uptake, and it's still among the fastest, smallest, and arguably simplest DOM rendering libraries out there. The API is extremely flexible and has worked well for a wide variety of use cases - we haven't seen a lot of places where we want to drastically change the API based on current usage. There are some long-standing requests - like static bindings and spread expressions - that still pop-up every once and awhile, but don't seem to be urgent deal-breakers for most users.
So we're in a good place and are not out looking for reasons to release a new major version - we really value API stability! - but we have some new goals, the browser landscape is changing, and we've been able to see what the common usage patterns are. We have a chance to integrate this and make an even better lit-html.
The team has been experimenting with some ideas for a lit-html 2.0, and it seems like a good time to write down some of the goals and possibilities to get feedback on them. Note that nothing is set in stone, and some of the changes we might be able to back-port into lit-html 1.x
Goals
Backwards compatibility for most users
The simplicity of the main public APIs -
html
andrender
- has worked really well, and has even been adopted by other libraries. So we want to keep that, and want API changes to be backward compatible for most users. This also includes users of directives: the built-in directives should be backwards compatible. Far fewer developers write directives though, so we're willing to consider breaking changes there.Backwards compatibility is especially important for internal Google use. Google's monorepo only allows one version of a library, so we'll have to update all of Google at once.
Performance
We always want to be as fast as possible :) Some changes that can make us faster are technically breaking though, and require a new major version. Ideas for improving perf range from removing abstractions, to doing less DOM work on template prep, to building an optional template pre-compiler. We also want to drive down the cost of template composition as much as possible, and optimize more for first render over updates.
Memory efficiency
lit-html is already decent with respect to memory consumption, but we can do better. Our architecture inherently requires memory usage between renders to store information for expressions (what we call "parts"). We can reduce this with API changes that let us keep fewer objects with fewer fields around. We especially want to optimize for the more common case of single-valued bindings.
Size reductions
lit-html is currently weighing in at ~3.4KB minzipped. We would love to get this back under ~3K. Removing some abstractions and browser bug workarounds can help, as well as exporting fewer public symbols so minification can work better. More concise modern DOM APIs are available if we can change browser support.
In addition, we can look at publishing a minified, bundled build so that we can crank the minifier settings to 11 and ensure the result works correctly. While we believe minification is ideally an app concern, we realize that apps won't know all the transforms that can be safely applied to our code.
Developer Experience
We would like to improve error messages, and not let publishing minified code negatively impact development. We'll look into producing both production and development builds.
Easier SSR
We are still working on SSR with the 1.x codebase, but we have learned a lot about how parts of the API make it easier or harder to implement SSR. The largest impediment is the sheer amount of flexibility our API gives developers. lit-html is in some ways more of a template library construction kit than just one template library. With pluggable template factories, template processors, polymorphic template results, and the ability to dynamically construct and insert parts into the DOM, we can't guarantee that we can represent all templates when SSR'ing, or understand how to reconstruct the JavaScript relationship between template constructs when hydrating.
Our SSR system will only be reliable when using a pretty non-customized version of lit-html, as the vast majority of users do. We've seen very little usage of these customization features, but their existence limits us, so we're going to explore removing some of them. We especially need to hear from developers using these features first though.
In addition, directives are a rough spot for SSR. To be server-renderable, directives currently need to stick within a subset of the API during first-render. We want a new directive API clarifies the allowable APIs and works on the server by default.
Better compile target
Some partners have existing template systems they may be interested in compiling to lit-html. Some new features would be useful for this, like unbalanced tags, statically verifiable spread attribute bindings, tag-name position bindings, and XSS-safe CSS bindings.
New features
Long-standing requests like static and spread bindings should be looked at, especially now that all current browsers correctly implement tagged template literals (which was preventing static bindings before now).
API cleanup
Some parts of the API are less-than-ideal and we shipped 1.0 knowing that we'd like to improve them. The directive and part APIs are prime examples. We want to make directives easier to write, and as mentioned, make them SSR compatible by default.
Potential Changes
Reduce the API surface
We see very little evidence of broad usage of extension and customization points like TemplateProcessor, TemplateFactory, and freely constructible Parts. Removing these abstractions would eliminate code, allow some APIs to be minified that weren't before, and reduce function calls and indirections in very hot code paths.
As noted in goals, we want the vast majority of existing templates to work as-is in lit-html 2.0.
Prod and dev builds
We don't want minified code to make debugging more difficult, so we should create an unminified development build with nice error messages and maybe even more template validation. We'll publish the production build by default so that apps don't accidentally deploy the dev build, and allow the development build to be used via various module replacement / aliasing tools, such as @rollup/plugin-alias.
Remove the workaround for broken template literals in Safari
Safari 12 had a bug where it would occasionally garbage collect template literal strings that shouldn't have been. This caused lit-html's caching to fail and re-render DOM that should have been updated. Our workaround was to use a secondary cache keyed by the joined strings of the template. This works without UA sniffing, but this has a perf and memory impact for all browsers. We should remove the workaround and consider Safari 12 to be a browser that doesn't support ES6 properly.
Simplify template caching by not allowing template tags to be dynamic.
Currently we cache by both the tag type (svg or html) and the template strings object. This lets you dynamically switch the template tag, but… who does that? We can eliminate a cache lookup completely by only caching by the template strings object.
Better HTML scanning
Before templates are parsed by the browser, lit-html prepares the HTML string by inserting markers where expressions are. We need to insert comments in some places, like text nodes, and non-comments in others, like attribute values. Our current scanner is good, but sometimes thinks that an expression is in an attribute when it's in text. This means we have to search all text nodes for markers, which has a high cost in template prep. If we can make a more precise scanner, so that we always insert comment markers in text nodes, we can skip processing them (except for raw text nodes like
<script>
,<style>
, and<textarea>
) and speed up template prep and thus first render.There is a careful balance here because the current scanner is so small and we want to reduce code size overall. We'll have to be lucky and/or good to make this work. If it does work we'll be able to support new binding types like spread and tag-name - or at least we'll be able to produce better error messages when people try to write them.
Spread bindings / Attribute position bindings
Attribute directives are very useful for running some logic on an element because the reference to the element is inherent in the template syntax, and the directive is run after the element is created. Sometimes you just need logic on the element and don't need to bind to an attribute, but in lit-html 1.x you need to invent an attribute name to place a binding. We've seen this when MWC used a directive for ripple:
With the better scanner we can add attribute position bindings:
Another example is Vaadin's
field()
directive:Which could be written as:
This will let us do nice things like ref and spread directives:
More template types
We have seen some need for template types like
attr
andcss
.attr
would be a template that just contains attributes (and possibly properties, events, etc), and would enable passing and setting multiple attributes in an XSS-safe manner.It's really an element partial, which has a number of interesting use cases, from parity with template systems like Closure Templates, to being able to declaratively set host attributes.
css
would be for bindings into CSS text in<style>
tags and possibly LitElement's styles property. It would make style bindings more XSS-safe by only allowing developer provided and sanitized values to be bound to CSS.LitElement's
css
is a bit different from a lit-html template, but if we can combine the two we can share CSS-safe values between them, and enable other component base-classes to use the Constructible StyleSheets-producing system.Remove template part's two-pass update
Due to how attributes with multi bindings interact with directives, we have a two-pass update process: first all parts in a template have their value set, then all parts are committed. This allows multi-binding attributes to set the attribute value only once. Removing this will reduce code, an entire pass over all parts in an app, and simplify the Part API for directive authors.
Remove attribute part committers
As part of removing two-pass updates we should remove the part/committer split. Currently we have a part for each attribute expression and a single committer shared between parts on the same attribute. This is extra work to setup and incurs extra memory overhead. An early version of lit-html had a multi-valued part type that could update with an array of values. We changed to allow parts to have their value set externally to rendering and to more closely align with the W3C Template Instantiation proposal. We don't want to let parts be updated externally anymore and we should only align with template instantiation once it's more concrete, so we should explore the old design again.
Remove async directives: until(), asyncAppend(), asyncReplace()
Dealing with asynchronicity in components is hard.
until()
and friends are helpful in some simple cases, but they are actually quite tricky to use correctly. It's pretty easy to generate a new Promise every render and pass it tountil()
so that the binding it's used in actually updates twice for every render: once for the fallback, and once again with the resolved value.Async templates are also hard to test if the async values aren't exposed outside the template in some way.
We're not 100% on this, but we suspect that async helpers should be moved up into the component model, leaving templates synchronous. It should still be possible to write an until-like directive, though such directives might require cooperation with a host component that can perform a re-render. TBD.
A better directive API
Directives have a few issues. First, they often directly interact with the DOM, making them difficult to SSR (our SSR library doesn't emulate the DOM). Second the API is stateless, but directives are really only needed when they either mutate the DOM or require state. Directives that require state then have to keep WeakMaps keyed off parts. This is cumbersome. Third, directives don't have a lifecycle, but we've had requests for one, and it could be useful for first-render performance.
We should investigate a stateful, class-based directive API. This may seem to fly in the face of other libraries eschewing classes, but we think we have something nice in the works.
Directives would extend or implement the
Directive
base class and implement one or more lifecycle methods, currentlyrender()
andupdate()
.render()
is for the initial render and SSR and can only return a lit-html value (including a template).update()
is called only on the client and can access a directive's part.lit-html will instantiate the directive instance and keep it around between renders. State can be class fields: no more WeakMaps!
This seems to be easy to implement and already much better for SSR. We'll also investigate a disconnect callback to allow cleaning up any held resources.
Other changes to explore for directives include making it less likely that they need to directly manipulate DOM or Part internals. Handling the
nothing
sentinel in the core library so that it removes attributes meansifDefined()
doesn't need to callremoveAttribute()
. Adding part state detach and attach methods meanscache()
andrepeat()
don't have to move the DOM for a part.Better animation support
When directives have lifecycles, we may be able to tie them into the host component's lifecycle to better enable animation directives. Animation techniques like FLIP work by measuring layout before DOM changes. If we can get a performant disconnected callback, we might be able to get a performant beforeRender() callback too, which would allow this. Another option would be to let the directive directly get a reference to the host component in its constructor and set a before render callback on the host, an API that would have to be added to LitElement and other component hosts.
Remove support for some data types
lit-html takes care to do nice things with data types like Symbol and arrays in attribute bindings. This costs bytes and exists on the hot paths. We can require developers to use patterns like
String(symbol)
andarray.join(' ')
instead. We'll obviously keep array support for text bindings so thatarray.map((i) => html
${i})
still works.Move IE11 support out of the core library
IE11 usage is dropping and we'd love to drop it, but we probably can't do it quite yet since some of our customers are stuck supporting it for a while. We might be able to change our support policy and move any additional code we need for IE11 and Edge Classic into auxiliary files that are loaded only by the applications that need it. lit-html (and by extension LitElement) wouldn't support IE11 by default then, and apps that don't need IE support wouldn't pay the cost of it, but an app could include a top-level script tag that would patch lit-html and make it work.
We can also use more modern DOM APIs that will save some bytes and just require more polyfills for IE. There are some tricky parts of IE support we might not be able to patch in though. IE and Edge Classic iterate attributes in an undefined order, making it harder to associate them with expressions. In-order iteration isn't polyfillable. IE and Edge Classic also remove style and other attributes with invalid content, which happens when they're bound, so we'll need to still change bound attribute names.
There is a lot of experimentation to be done here.
Remove use of instanceof
Using
instanceof
to detect lit-html's own objects is problematic in cases where npm installs multiple versions of the lit-html package. It's also slow for false cases as it has to traverse the prototype chain (we need to measure and confirm this). We can move to looking for a sentinel own property onTemplateResult
and other objects.Template pre-compiler
While we think we can bring down template processing overhead a bit, we could possibly eliminate most of the work by pre-compiling templates. We need to let TemplateResult be able to carry a reference to pre-compiled template strings and part metadata, rather than just a template literal strings.
Pre-compiling templates may be an alternative way to supporting Safari 12, IE11, and Edge Classic since it skips some usage of template literals and DOM APIs that are problematic for them.
Next steps
That's a lot of potential changes! We'll be lucky if the majority of them pan out. Some changes, like new features, are potentially in conflict with goals for performance and code size. Other changes, like reducing configurability, may be hard on some customers.
One thing we know we need to do is drive the whole process from science, which means lots of benchmarking. Our current "Shack" benchmark, and the community JS framework benchmarks don't exercise enough of the features and code paths to give us enough information. So we'll develop better benchmarks, and a parameterized benchmark generator to be able to tell where performance tradeoffs start paying off (ie, benchmarks with adjustable mix of static vs dynamic content in a template).
We're experimenting with some of the ideas in this issue already, and need to figure out when, where, and how to integrate the changes into this repo. Details TBD.
Meanwhile, we encourage feedback on these ideas and any more that our users have.
The text was updated successfully, but these errors were encountered: