The 19-year-old continued his superb breakthrough season by finding the net twice in six second-half minutes to become the youngest Italian to score two goals in a Champions League match.
Porto substitute Adrian Lopez, however, quickly responded with what could prove to be a crucial away goal.
The result ended a five-match winning run in Europe for Porto, who will welcome Roma to the Estadio do Dragao for the second leg on March 6.
"It’s an incredible night, I am speechless and never would have expected such a thing,” Zaniolo said.
"However, we’ve got the second leg coming up, we shouldn’t have conceded the goal and we’ve got to think as a unit.
“We allowed Porto practically nothing, so we have to do the same in the second leg to qualify. I worked hard tonight, but I am very happy to do that for this team.”
Roma's Edin Dzeko struck the post with the best chance of an uneventful first half, but the game opened up after the break as Bryan Cristante stung Porto goalkeeper Iker Casillas’ palms with a fierce shot and Porto midfielder Danilo headed a corner narrowly wide.
Zaniolo broke the deadlock after 70 minutes when he raced onto a Dzeko layoff to fire low into the corner, before Dzeko’s long-range shot came back off the post six minutes later for the teenager to tap in his second.
Porto responded almost immediately when Lopez latched onto a miscued effort by Tiquinho Soares to fire in from an angle with 10 minutes remaining.
It was Roma's first win over Porto in five attempts, having lost their last meeting in Rome 3-0 in 2016 in the second leg of a 4-1 aggregate Champions League playoff victory for the Portuguese side.
(Editing by Toby Davis)