-
Notifications
You must be signed in to change notification settings - Fork 10.1k
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
Strip insignificant whitespace at compile time #23385
Strip insignificant whitespace at compile time #23385
Conversation
Would be interesting to consider what it take to backport the moral equivalent of this to Razor (views/pages). Folks have been asking for the ability to have Razor auto-strip whitespace for a long time and I like the opt-in/out via a directive approach here. |
@@ -1,6 +1,6 @@ | |||
@using BasicWebSite.Services | |||
@inject WeatherForecastService ForecastService | |||
|
|||
@preservewhitespace true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it means I don't have to change the test case in a weird way (which wouldn't really be beneficial since the MVC test case isn't meant to be about how the component does its rendering internally).
internal static class ComponentPreserveWhitespaceDirective | ||
{ | ||
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( | ||
"preservewhitespace", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yaay precendence: http://www.xmlplease.com/xml/xmlspace/#s3.
@@ -217,6 +217,7 @@ private static void AddComponentFeatures(RazorProjectEngineBuilder builder) | |||
ComponentLayoutDirective.Register(builder); | |||
ComponentPageDirective.Register(builder); | |||
ComponentTypeParamDirective.Register(builder); | |||
ComponentPreserveWhitespaceDirective.Register(builder); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might have to introduce a new LangVersion and do this only for RazorLangVersion = 5.0 or newer. We don't want this directive to start working for 3.0 projects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup need to introduce a new lang version for this. The biggest concern is without a lang version if you use a "new" IDE aka 16.8 VS without a new lang version it'll offer this directive in completions and wont complain when you use it in your files. However, the second you build this will cause build-time errors because it'll be using a different SDK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, sounds good. Thanks for the explanation!
@pranavkm has volunteered to help with defining the new lang version, since apparently there are some subtleties has has context on related to controlling which Razor extension gets used inside VS.
Hopefully we'll have this in time to all go in at once in time for preview 7 (but if not, we could define the razorlangversion separately afterwards).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
This PR is very important, and our project is plagued by this problem. |
Ping @NTaylorMullen @ajaybhargavb for re-review now. Hoping this can be merged today for inclusion in preview 7! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! None of my questions/suggestions should be blocking for this release and can be done in a follow-up.
builder => | ||
{ | ||
builder.AddBooleanToken(ComponentResources.PreserveWhitespaceDirective_BooleanToken_Name, ComponentResources.PreserveWhitespaceDirective_BooleanToken_Description); | ||
builder.Usage = DirectiveUsage.FileScopedMultipleOccurring; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason for this to be FileScopedMultipleOccurring
instead of FileScopedSinglyOccurring
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the multiple occurring was needed for overriding, but if you’re saying that will still work with single occurring I’ll change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my understand it is to allow multiple directives in the same file. Singly occurring should still allow overriding what was set in _Imports.razor. @NTaylorMullen can you confirm?
if (node.Children[childIndex] is CSharpCodeIntermediateNode) | ||
{ | ||
childIndex -= RemoveContiguousWhitespace(node.Children, TraversalDirection.Backwards, childIndex - 1); | ||
RemoveContiguousWhitespace(node.Children, TraversalDirection.Forwards, childIndex + 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trying to understand what is happening here. Why is this restarting the search from wherever it traversed to in the backwards direction? Why not just start it from childIndex + 1
without doing the childIndex -=
in the previous line. Could definitely use a comment here to explain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ll add a comment. We have to adjust the index here to account for having mutated the collection we’re iterating over.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, makes sense
description: description)); | ||
|
||
return builder; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this benefit from having an optional boolean token? So people can just write @preserveWhitespace
if they want the old behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH this will be so rarely used I’d prefer to force the more explicit usage. But if you feel differently we can always change this in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern is not specific to this directive but do you envision more boolean directives in the future that might benefit from this being optional? If so, I think it'll be a little confusing for the users if there are some boolean directive that requires explicit values and some that don't. I don't feel strongly but just something to consider.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally reasonable points. Perhaps at the time where we first feel it's valuable to add optional boolean support, we can decide whether or not to apply that feature here too, as that won't be a breaking change.
@@ -1417,6 +1418,22 @@ private void ParseExtensibleDirective(in SyntaxListBuilder<RazorSyntaxNode> buil | |||
return; | |||
} | |||
break; | |||
|
|||
case DirectiveTokenKind.Boolean: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does the tooling experience currently look like?
Need to update DesignTimeDirectiveTargetExtension to provide the appropriate design time support so that it suggests true
and false
as options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, good point!
Thanks for the feedback @ajaybhargavb! Since you’ve pointed out the remaining bits don’t need to block merging, and to ensure this makes it into preview 7, I think we should merge this now then I’ll do a follow-up that can go into preview 8. @mkArtakMSFT Can you merge this for preview 7? Thanks! |
* Define @preservewhitespace directive attribute * Have ComponentWhitespacePass respect preservewhitespace option * Tests for overriding the preservewhitespace option (and fix implementation) * Begin adding test infrastucture for ComponentWhitespacePass * Remove leading/trailing whitespace from markup elements recursively * Update baselines * Add test showing we don't remove whitespace between sibling elements * Remove whitespace before/after C# code statements (not expressions) * Update baselines * Slight improvements to test * Remove leading/trailing whitespace inside component child content * Update baselines * Fix Razor tests * Fix MVC test * Better fix for MVC test * CR: Make ComponentPreserveWhitespaceDirective conditional on langversion >= 5.0 Commit migrated from dotnet/aspnetcore@eb7693157894
This is part of the perf optimization effort for Blazor.
From profiling, I've found that in intense rendering scenarios, we spend a lot of time dealing with whitespace. Even as much as half the total time. This is because (1) a lot of the nodes are just whitespace, and (2) whitespace is extra-expensive to insert because it gets treated as markup fragments (which trigger a separate HTML parsing call on each insertion).
As an example, consider this typical component code:
Looks harmless. But it generates whitespace nodes all over the place, due to spaces and linebreaks:
It produces more whitespace nodes than anything else, and all of them are completely useless! They take up time in rendering, diffing, and in DOM-updating.
Proposed solution
Just like Angular and React do, we should by default trim insignificant whitespace nodes. Technically this will be a breaking change, because in theory all whitespace can be "significant" (e.g., if you set
white-space: pre
in CSS). However I think that:In this PR, I've beefed up
ComponentWhitespacePass
so that it removes:Notably, I'm not proposing to remove whitespace from between sibling elements, whether or not they are block elements. That's because (1) I don't want to hard-code in knowledge about which element types are typically inline versus block, as it can be changed anyway via CSS and Razor isn't meant to be coupled to HTML, and (2) that's one of the behaviors that upsets/confuses React developers, and (3) it's just not very common compared with leading/trailing whitespace given typical shapes of HTML.
I've also added a new directive that you can use to get back the older whitespace-preserving behavior if you want:
Like other directives, you can put this into your
_Imports.razor
if you want the preference to apply to an entire subdirectory or even your entire project. And you can override on a nested basis (e.g.,@preservewhitespace false
to get the stripping back on a specific component).Since this is technically a breaking change, I'm keen to include this in the preview 7 release to get the maximum opportunity for developer feedback.