Fast on the project laptop, slow on the customer's phone
A supplier portal I reviewed took eleven seconds to show a purchase order list to the buyers who opened it every morning. On the project team's laptops the same page came back in under two seconds, and that was the number in the status report. They had tested with administrator accounts, on the office network, against sixty demo orders.
The people who judge a portal are outside your building on hardware nobody on the project chose. They sign in on a four-year-old phone over mobile data, carrying a contact record with three web roles, and every row they see is filtered by a permission chain that barely runs for an administrator. The picture an internal admin gets is systematically optimistic, and it worsens with every record the business adds after go-live.
Most slow Power Pages sites are slow for reasons sitting in plain view in the configuration. Work through them in order. Server-side data work first, then what the browser downloads, then measurement, because a site you have not measured honestly sends you off to fix the wrong thing.
Every row the page renders goes through the permission chain
Power Pages does not fetch rows and then hide the ones a visitor should not see. Access is resolved on the way in. A sign-in resolves to a Dataverse contact, the contact carries web roles, the roles carry table permissions, and each permission names a scope that may walk a relationship from the record back to that contact. A contact holding more roles than it needs costs you on every request, and the identity decisions behind that join are hard to reverse.
A page showing one record pays for that resolution once. A page showing eighty rows pays for it eighty times, and a scope that walks two relationships to reach the visitor walks them again on every row. Stack three permissions on one table because three pages each needed slightly different access, and every row is evaluated against all three.
Permission design is a performance question as well as a security one, which is not how most teams hold it. The expensive pattern is a broad permission narrowed afterwards by a filter on the list, because the wide evaluation still happens first. Set the scope right at the permission, keep the count per table small, and keep relationship depth shallow. The access review in what to check before a Power Pages site goes public reads the same rows, so do both together.
A list that fetches more than the page shows
A list on a Power Pages site is driven by a Dataverse view, and the view decides what the query asks for. The template decides what gets printed. Where the two disagree, the page pays the difference on every row, including the lookups resolved so a column can display a name.
Reusing the model-driven app view is the usual cause. Someone points the portal list at the view the internal team already uses, which carries twenty columns because an account manager wanted them all. The page prints six. The other fourteen are retrieved, permission-checked and thrown away.
Page size does the same damage from the other end. A list set to fifty rows renders fifty for a phone showing four at a time, and switching on the record count adds a second pass over the same filtered set to print a total nobody reads. Build a view that exists only for the portal, with the columns the page prints and a page size somebody chose on purpose.
Lookups multiply, and Liquid loops multiply faster
Every relationship traversed on a rendered row multiplies the cost. A list of orders showing the account name, its primary contact and the order's approver makes three traversals per row, and each one runs the permission chain for the table at the far end. Fifty rows and three traversals mean a hundred and fifty evaluations before the browser sees a pixel.
Where you can, carry the value instead of fetching it. A denormalised column holding the account name on the order row, written by the same server-side logic that creates the record, costs one write and saves a traversal on every read. Nobody calls that elegant. On a list four hundred suppliers open every morning, it is still the right call.
Liquid runs on the server before the page is sent, so what it costs never shows up in the browser tools a maker opens first. A template that fetches records inside a loop is the most common cause of a page that behaves in testing and collapses under real data, because the cost is the loop count times the fetch and the loop count in testing was six. Hoist the query out of the loop, fetch the set once, hold it in a variable and read that variable inside. If the set cannot be retrieved in one query, the page is asking something the data model does not answer, and the fix belongs in the model.
The images nobody compressed and the script on every page
Once the server-side work is sane, the browser's share is mostly weight. Unoptimised images are the most common and easiest win on a content-heavy portal, and I have yet to review a site where somebody had already done it. A hero image exported from a design tool at three thousand pixels wide, served to a phone that draws it at four hundred, spends most of the page load on nothing.
Resize images to the largest size they will be displayed at, save them in a modern format, and put a hard limit into the content authoring guidance, because whoever uploads next month's supplier bulletin will not think about it. A ten megabyte guidance file on a landing page costs nothing until someone taps it on a train.
Web files and scripts are the second layer. Sites accumulate a chart library added for one report page, a date picker added for one form and a tracking script somebody wanted for a campaign, and all three end up in the header where every page pays for them. Move anything serving a single page onto that page.
Caching is where a portal is genuinely harder than a public marketing site. A page rendering per-user filtered data cannot be cached aggressively, because the cached copy would be one visitor's rows handed to the next. Split the page deliberately instead. Navigation, static content, stylesheets, images and scripts are the same for everyone and should be cached hard. The filtered list is the only part that has to render on request.
Three tests that find most of it before customers do
Test as an anonymous visitor and as a low-privilege authenticated user, in a private window, with the administrator session closed. An administrator skips most of the permission evaluation that costs a real visitor their wait, so an admin timing measures nothing a customer experiences. Make a test contact with exactly the web roles a customer gets.
Test with production-scale data. Almost every portal performance problem is invisible at demo scale, because a permission chain over sixty rows and one over sixty thousand look identical until one of them is real. Load a production-sized volume into the test environment first, and watch Dataverse capacity while you are there.
Test on a phone on a mobile network, not on a phone sitting on the office wifi. Those three together find most of what your customers would otherwise find for you. Write the numbers down before launch, so the argument three weeks later is about a measurement rather than whose laptop felt fine.
- Time to first render on the busiest list page, on a phone over mobile data.
- Table permissions that apply per row, and the relationship depth of each scope.
- Column count and page size of every view a portal list is bound to.
- Every Liquid template checked for a query inside a loop.
- Total image weight per page, and the largest single image served to a phone.
- What the header and footer load on every page, and which pages need it.
- Which parts of each page are identical for all visitors and can be cached.
- The same list timed again at production data volume before the go-live call.
Start with the page your customers open most
Open the list page your customers hit hardest, in a private window, signed in as a test contact with a customer's web roles, on a phone over mobile data. Time it, then count the table permissions on the table behind that list and the columns in the view it is bound to.
In my experience the number that explains the wait is one of those two, and both can be fixed in an afternoon by whoever built the site. Do that before a buyer does it for you, in an email to your service desk, nine days after launch.


